diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..5f093eae9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,311 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = false +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = false +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = false +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = normal +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = true +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 1 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = normal +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = true +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = normal +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*, |, * +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = true +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 1 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = normal +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = normal +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = true +ij_java_ternary_operation_wrap = normal +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = true +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true + +[*.xml] +ij_xml_align_attributes = false +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal \ No newline at end of file diff --git a/README.md b/README.md index 9753c17ff..7b92bcc50 100755 --- a/README.md +++ b/README.md @@ -2,16 +2,20 @@ [![Build Status](https://travis-ci.org/neo4j-contrib/spatial.png)](https://travis-ci.org/neo4j-contrib/spatial) -Neo4j Spatial is a library facilitating the import, storage and querying of spatial data in the [Neo4j open source graph database](http://neo4j.org/). +Neo4j Spatial is a library facilitating the import, storage and querying of spatial data in +the [Neo4j open source graph database](http://neo4j.org/). -This projects manual is deployed as part of the local build as the [Neo4j Spatial Manual](http://neo4j-contrib.github.io/spatial). +This projects manual is deployed as part of the local build as +the [Neo4j Spatial Manual](http://neo4j-contrib.github.io/spatial). ![Open Street Map](http://neo4j-contrib.github.io/spatial/0.24-neo4j-3.1/images/one-street.png "Open Street Map") ## History -This library began as a collaborative vision between Neo-Technology and [Craig Taverner](https://github.com/craigtaverner) in early 2010. -The bulk of the initial work was done by [Davide Savazzi](https://github.com/svzdvd) as part of his 2010 Google Summer of Code (GSoC) project +This library began as a collaborative vision between Neo-Technology +and [Craig Taverner](https://github.com/craigtaverner) in early 2010. +The bulk of the initial work was done by [Davide Savazzi](https://github.com/svzdvd) as part of his 2010 Google Summer +of Code (GSoC) project with Craig as mentor, as a project within the OSGeo GSoC program. In 2011 and 2012 two further GSoC projects contributed, the last of which saw Davide return as mentor. @@ -27,7 +31,7 @@ Over the years there have been various efforts to make the library more appropri * IndexProvider mechanism (used during Neo4j 1.x and 2.x server, and removed for Neo4j 3.0) * 0.23: Addition of Cypher procedures for Neo4j 3.0 * 0.24: Addition of a high performance bulk importer to the in-graph RTree index -* 0.25: Addition of GeoHash indexes for point layers +* 0.25: Addition of GeoHash indexes for point layers * 0.26: Support for native Neo4j point types * 0.27: Major port to Neo4j 4.x which deprecated many of the Neo4j API's the library depended on * 0.29: Port to Neo4j 5.13 @@ -51,16 +55,16 @@ This has meant that the spatial library needed a major refactoring to work with for the current transaction, and only the specific surface designed for embedded use not have that. * The library made use of Lucene based explicit indexes in many places. The removal of support for explicit indexes required completely new solutions in sevaral places: - * The `OSMImporter` will instead now use normal Neo4j schema indexes (introduced in 2.0). - However, these can only be created in separate index transactions. - Due to the new transaction model this requires stopping the import transaction, - starting an index transaction, and then restarting the import transaction. - All of this is incompatible with procedures, which already have an incoming, non-stoppable transaction. - The solution to this second problem was to run the actual import in another thread. - This has the additional benefit of retaining the original batch-processing capabilities. - The negative consequence of this it that it requires modifying the security model of the procedure context. - * The `ExplicitIndexBackedPointIndex` has been modified to instead use a schema index. - This required similar tricks to those employed in the `OSMImporter` described above. + * The `OSMImporter` will instead now use normal Neo4j schema indexes (introduced in 2.0). + However, these can only be created in separate index transactions. + Due to the new transaction model this requires stopping the import transaction, + starting an index transaction, and then restarting the import transaction. + All of this is incompatible with procedures, which already have an incoming, non-stoppable transaction. + The solution to this second problem was to run the actual import in another thread. + This has the additional benefit of retaining the original batch-processing capabilities. + The negative consequence of this it that it requires modifying the security model of the procedure context. + * The `ExplicitIndexBackedPointIndex` has been modified to instead use a schema index. + This required similar tricks to those employed in the `OSMImporter` described above. * Neo4j 4.0 runs only in Java 11, and until recently GeoTools did not support newer Java versions. It was therefor necessary to upgrade the GeoTools libraries to version 24.2. This in turn required a re-write of the Neo4jDataStore interface since the older API had @@ -93,7 +97,7 @@ Consequences of the port to Neo4j 4.x: * The new DataStore API is entirely untested in GeoServer, besides the existing unit and integration tests. * The need to manage threads and create schema indexes results in the procedures requiring unrestricted access to internal API's of Neo4j. - + This last point means that you need to set the following in your `neo4j.conf` file: ``` @@ -101,7 +105,8 @@ dbms.security.procedures.unrestricted=spatial.* ``` If you are concerned about the security implications of unrestricted access, my best advice is to review -the code and decide for yourself the level of risk you face. See, for example, the method `IndexAccessMode.withIndexCreate`, +the code and decide for yourself the level of risk you face. See, for example, the +method `IndexAccessMode.withIndexCreate`, which adds index create capabilities to the security model. This means that users without index creation privileges will be able to create the necessary spatial support indexes described above. @@ -118,11 +123,12 @@ Please report any mistakes as issues, or consider raising a pull-request with an The key concepts of this library include: -* Allow the user to model geograph data in whatever way they wish, through providing an adapter (extend `GeometryEncoder`). +* Allow the user to model geograph data in whatever way they wish, through providing an adapter ( + extend `GeometryEncoder`). Built-in encoders include: - * WKT and WKB stored as properties of nodes - * Simple points as properties of nodes (two doubles, or a double[] or a native Neo4j `Point`) - * OpenStreetMap with complex geometries stored as sub-graphs to reflect the original topology of the OSM model + * WKT and WKB stored as properties of nodes + * Simple points as properties of nodes (two doubles, or a double[] or a native Neo4j `Point`) + * OpenStreetMap with complex geometries stored as sub-graphs to reflect the original topology of the OSM model * Multile CoordinationReferenceSystem support using GeoTools * Support the concept of multiple geographic layers, each with its own CRS and Index * Include an index capable of searching for complex geometries (in-graph RTree index) @@ -134,18 +140,19 @@ Some key features include: * Utilities for importing from ESRI Shapefile as well as Open Street Map files * Support for all the common geometry types * An RTree index for fast searches on geometries -* Support for topology operations during the search (contains, within, intersects, covers, disjoint, etc.) -* The possibility to enable spatial operations on any graph of data, regardless of the way the spatial data is stored, as long as an adapter is provided to map from the graph to the geometries. +* Support for topology operations during the search (contains, within, intersects, covers, disjoint, etc.) +* The possibility to enable spatial operations on any graph of data, regardless of the way the spatial data is stored, + as long as an adapter is provided to map from the graph to the geometries. * Ability to split a single layer or dataset into multiple sub-layers or views with pre-configured filters * Server Plugin for Neo4j Server 2.x and 3.x - * REST API for creating layers and adding nodes or geometries to layers - * IndexProvider API (2.x only) for Cypher access using `START node=node:geom({query})` - * Procedures (3.x only) for much more comprehensive access to spatial from Cypher + * REST API for creating layers and adding nodes or geometries to layers + * IndexProvider API (2.x only) for Cypher access using `START node=node:geom({query})` + * Procedures (3.x only) for much more comprehensive access to spatial from Cypher ## Index and Querying ## - -The current index is an RTree index, but it has been developed in an extensible way allowing for other indices to be added if necessary. +The current index is an RTree index, but it has been developed in an extensible way allowing for other indices to be +added if necessary. The spatial queries implemented are: * Contain @@ -159,25 +166,36 @@ The spatial queries implemented are: * Touch * Within * Within Distance - + ## Building ## -The simplest way to build Neo4j Spatial is by using maven. Just clone the git repository and run +The simplest way to build Neo4j Spatial is by using maven. Just clone the git repository and run ~~~bash mvn install ~~~ -This will download all dependencies, compiled the library, run the tests and install the artifact in your local repository. -The spatial plugin will also be created in the `target` directory, and can be copied to your local server using instructions on the spatial server plugin below. +This will download all dependencies, compiled the library, run the tests and install the artifact in your local +repository. +The spatial plugin will also be created in the `target` directory, and can be copied to your local server using +instructions on the spatial server plugin below. ## Layers and GeometryEncoders ## -The primary type that defines a collection of geometries is the Layer. A layer contains an index for querying. In addition a Layer can be an EditableLayer if it is possible to add and modify geometries in the layer. The next most important interface is the GeometryEncoder. +The primary type that defines a collection of geometries is the Layer. A layer contains an index for querying. In +addition a Layer can be an EditableLayer if it is possible to add and modify geometries in the layer. The next most +important interface is the GeometryEncoder. -The DefaultLayer is the standard layer, making use of the WKBGeometryEncoder for storing all geometry types as byte[] properties of one node per geometry instance. +The DefaultLayer is the standard layer, making use of the WKBGeometryEncoder for storing all geometry types as byte[] +properties of one node per geometry instance. -The OSMLayer is a special layer supporting Open Street Map and storing the OSM model as a single fully connected graph. The set of Geometries provided by this layer includes Points, LineStrings and Polygons, and as such cannot be exported to Shapefile format, since that format only allows a single Geometry per layer. However, OMSLayer extends DynamicLayer, which allow it to provide any number of sub-layers, each with a specific geometry type and in addition based on a OSM tag filter. For example you can have a layer providing all cycle paths as LineStrings, or a layer providing all lakes as Polygons. Underneath these are all still backed by the same fully connected graph, but exposed dynamically as apparently separate geometry layers. +The OSMLayer is a special layer supporting Open Street Map and storing the OSM model as a single fully connected graph. +The set of Geometries provided by this layer includes Points, LineStrings and Polygons, and as such cannot be exported +to Shapefile format, since that format only allows a single Geometry per layer. However, OMSLayer extends DynamicLayer, +which allow it to provide any number of sub-layers, each with a specific geometry type and in addition based on a OSM +tag filter. For example you can have a layer providing all cycle paths as LineStrings, or a layer providing all lakes as +Polygons. Underneath these are all still backed by the same fully connected graph, but exposed dynamically as apparently +separate geometry layers. ## Examples ## @@ -187,11 +205,15 @@ Spatial data is divided in Layers and indexed by a RTree. ~~~java GraphDatabaseService database = new GraphDatabaseFactory().newEmbeddedDatabase(storeDir); - try { - ShapefileImporter importer = new ShapefileImporter(database); - importer.importFile("roads.shp", "layer_roads"); - } finally { - database.shutdown(); + try{ +ShapefileImporter importer = new ShapefileImporter(database); + importer. + +importFile("roads.shp","layer_roads"); + }finally{ + database. + +shutdown(); } ~~~ @@ -204,7 +226,10 @@ CALL spatial.importShapefileToLayer('layer_roads','roads.shp') ### Importing an Open Street Map file ### -This is more complex because the current OSMImporter class runs in two phases, the first requiring a batch-inserter on the database. There is ongoing work to allow for a non-batch-inserter on the entire process, and possibly when you have read this that will already be available. Refer to the unit tests in classes TestDynamicLayers and TestOSMImport for the latest code for importing OSM data. At the time of writing the following worked: +This is more complex because the current OSMImporter class runs in two phases, the first requiring a batch-inserter on +the database. There is ongoing work to allow for a non-batch-inserter on the entire process, and possibly when you have +read this that will already be available. Refer to the unit tests in classes TestDynamicLayers and TestOSMImport for the +latest code for importing OSM data. At the time of writing the following worked: ~~~java OSMImporter importer = new OSMImporter("sweden"); @@ -251,9 +276,15 @@ WITH "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))" as polyg CALL spatial.intersects('layer_roads',polygon) YIELD node RETURN node.name as name ~~~ -For further Java examples, refer to the test code in the [LayersTest](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/LayersTest.java) and the [TestSpatial](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/TestSpatial.java) classes. +For further Java examples, refer to the test code in +the [LayersTest](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/LayersTest.java) +and +the [TestSpatial](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/TestSpatial.java) +classes. -For further Procedures examples, refer to the code in the [SpatialProceduresTest](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java) class. +For further Procedures examples, refer to the code in +the [SpatialProceduresTest](https://github.com/neo4j-contrib/spatial/blob/master/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java) +class. ## Neo4j Spatial Geoserver Plugin ## @@ -262,7 +293,8 @@ However, regular testing of new releases of Neo4j Spatial against GeoServer is n and so we welcome feedback on which versions are known to work, and which ones do not, and perhaps some hints as to the errors or problems encountered. -Each release of Neo4j Spatial builds against a specific version of GeoTools and should then be used in the version of GeoServer that corresponds to that. +Each release of Neo4j Spatial builds against a specific version of GeoTools and should then be used in the version of +GeoServer that corresponds to that. The list of releases below starting at Neo4j 2.0.8 were built with GeoTools 9.0 for GeoServer 2.3.2, but most release for Neo4j 3.x were ported to GeoTools 14.4 for GeoServer 2.8.4. @@ -278,11 +310,15 @@ This has not been tested at all in any GeoTools enabled application, but could p ### Deployment into Geoserver ### -* unzip the `target/xxxx-server-plugin.zip` and the Neo4j libraries from your Neo4j download under `$NEO4J_HOME/lib` into `$GEOSERVER_HOME/webapps/geoserver/WEB-INF/lib` +* unzip the `target/xxxx-server-plugin.zip` and the Neo4j libraries from your Neo4j download under `$NEO4J_HOME/lib` + into `$GEOSERVER_HOME/webapps/geoserver/WEB-INF/lib` * restart geoserver * configure a new workspace -* configure a new datasource neo4j in your workspace. Point the "The directory path of the Neo4j database:" parameter to the relative (form the GeoServer working dir) or aboslute path to a Neo4j Spatial database with layers (see [Neo4j Spatial](https://github.com/neo4j/spatial)) -* in Layers, do "Add new resource" and choose your Neo4j datastore to see the exisitng Neo4j Spatial layers and add them. +* configure a new datasource neo4j in your workspace. Point the "The directory path of the Neo4j database:" parameter to + the relative (form the GeoServer working dir) or aboslute path to a Neo4j Spatial database with layers ( + see [Neo4j Spatial](https://github.com/neo4j/spatial)) +* in Layers, do "Add new resource" and choose your Neo4j datastore to see the exisitng Neo4j Spatial layers and add + them. ### Testing in GeoServer trunk ### @@ -299,14 +335,16 @@ This has not been tested at all in any GeoTools enabled application, but could p mvn clean install ~~~ -* check that you can run the web app as of [The GeoServer Maven build guide](http://docs.geoserver.org/latest/en/developer/maven-guide/index.html#running-the-web-module-with-jetty) +* check that you can run the web app as + of [The GeoServer Maven build guide](http://docs.geoserver.org/latest/en/developer/maven-guide/index.html#running-the-web-module-with-jetty) ~~~bash cd src/web/app mvn jetty:run ~~~ -* in `$GEOSERVER_SOURCE/src/web/app/pom.xml` https://svn.codehaus.org/geoserver/trunk/src/web/app/pom.xml, add the following lines under the profiles section: +* in `$GEOSERVER_SOURCE/src/web/app/pom.xml` https://svn.codehaus.org/geoserver/trunk/src/web/app/pom.xml, add the + following lines under the profiles section: ~~~xml @@ -321,7 +359,9 @@ This has not been tested at all in any GeoTools enabled application, but could p ~~~ -The version specified on the version line can be changed to match the version you wish to work with (based on the version of Neo4j itself you are using). Too see which versions are available see the list at [Neo4j Spatial Releases](https://github.com/neo4j-contrib/m2/tree/master/releases/org/neo4j/neo4j-spatial). +The version specified on the version line can be changed to match the version you wish to work with (based on the +version of Neo4j itself you are using). Too see which versions are available see the list +at [Neo4j Spatial Releases](https://github.com/neo4j-contrib/m2/tree/master/releases/org/neo4j/neo4j-spatial). * start the GeoServer webapp again with the added neo4j profile @@ -332,35 +372,36 @@ The version specified on the version line can be changed to match the version yo * find Neo4j installed as a datasource under http://localhost:8080 - ## Using Neo4j Spatial with uDig ## -For more info head over to [Neo4j Wiki on uDig](http://wiki.neo4j.org/content/Neo4j_Spatial_in_uDig) (This wiki is currently dead, but there appears to be a working mirror in Japan at http://oss.infoscience.co.jp/neo4j/wiki.neo4j.org/content/Neo4j_Spatial_in_uDig.html). +For more info head over to [Neo4j Wiki on uDig](http://wiki.neo4j.org/content/Neo4j_Spatial_in_uDig) (This wiki is +currently dead, but there appears to be a working mirror in Japan +at http://oss.infoscience.co.jp/neo4j/wiki.neo4j.org/content/Neo4j_Spatial_in_uDig.html). ## Using the Neo4j Spatial Server plugin ## The Neo4j Spatial Plugin is available for inclusion in the server version of Neo4j 2.x, Neo4j 3.x and Neo4j 4.x. * Using GeoTools 9.0 (for GeoServer 2.3.2): - * [v0.12 for Neo4j 2.0.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.12-neo4j-2.0.4/neo4j-spatial-0.12-neo4j-2.0.4-server-plugin.zip?raw=true) - * [v0.13 for Neo4j 2.1.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.13-neo4j-2.1.8/neo4j-spatial-0.13-neo4j-2.1.8-server-plugin.zip?raw=true) - * [v0.14 for Neo4j 2.2.7](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.14-neo4j-2.2.7/neo4j-spatial-0.14-neo4j-2.2.7-server-plugin.zip?raw=true) - * [v0.15.2 for Neo4j 2.3.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.15.2-neo4j-2.3.4/neo4j-spatial-0.15.2-neo4j-2.3.4-server-plugin.zip?raw=true) - * [v0.19 for Neo4j 3.0.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.19-neo4j-3.0.3/neo4j-spatial-0.19-neo4j-3.0.3-server-plugin.jar?raw=true) + * [v0.12 for Neo4j 2.0.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.12-neo4j-2.0.4/neo4j-spatial-0.12-neo4j-2.0.4-server-plugin.zip?raw=true) + * [v0.13 for Neo4j 2.1.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.13-neo4j-2.1.8/neo4j-spatial-0.13-neo4j-2.1.8-server-plugin.zip?raw=true) + * [v0.14 for Neo4j 2.2.7](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.14-neo4j-2.2.7/neo4j-spatial-0.14-neo4j-2.2.7-server-plugin.zip?raw=true) + * [v0.15.2 for Neo4j 2.3.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.15.2-neo4j-2.3.4/neo4j-spatial-0.15.2-neo4j-2.3.4-server-plugin.zip?raw=true) + * [v0.19 for Neo4j 3.0.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.19-neo4j-3.0.3/neo4j-spatial-0.19-neo4j-3.0.3-server-plugin.jar?raw=true) * Using GeoTools 14.4 (for GeoServer 2.8.4): - * [v0.25.1 for Neo4j 3.0.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.1-neo4j-3.0.8/neo4j-spatial-0.25.1-neo4j-3.0.8-server-plugin.jar?raw=true) - * [v0.25.3 for Neo4j 3.1.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.3-neo4j-3.1.4/neo4j-spatial-0.25.3-neo4j-3.1.4-server-plugin.jar?raw=true) - * [v0.25.4 for Neo4j 3.2.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.4-neo4j-3.2.8/neo4j-spatial-0.25.4-neo4j-3.2.8-server-plugin.jar?raw=true) - * [v0.25.5 for Neo4j 3.3.5](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.5-neo4j-3.3.5/neo4j-spatial-0.25.5-neo4j-3.3.5-server-plugin.jar?raw=true) - * [v0.26.2 for Neo4j 3.4.12](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.26.2-neo4j-3.4.12/neo4j-spatial-0.26.2-neo4j-3.4.12-server-plugin.jar?raw=true) - * [v0.26.2 for Neo4j 3.5.2](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.26.2-neo4j-3.5.2/neo4j-spatial-0.26.2-neo4j-3.5.2-server-plugin.jar?raw=true) + * [v0.25.1 for Neo4j 3.0.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.1-neo4j-3.0.8/neo4j-spatial-0.25.1-neo4j-3.0.8-server-plugin.jar?raw=true) + * [v0.25.3 for Neo4j 3.1.4](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.3-neo4j-3.1.4/neo4j-spatial-0.25.3-neo4j-3.1.4-server-plugin.jar?raw=true) + * [v0.25.4 for Neo4j 3.2.8](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.4-neo4j-3.2.8/neo4j-spatial-0.25.4-neo4j-3.2.8-server-plugin.jar?raw=true) + * [v0.25.5 for Neo4j 3.3.5](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.25.5-neo4j-3.3.5/neo4j-spatial-0.25.5-neo4j-3.3.5-server-plugin.jar?raw=true) + * [v0.26.2 for Neo4j 3.4.12](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.26.2-neo4j-3.4.12/neo4j-spatial-0.26.2-neo4j-3.4.12-server-plugin.jar?raw=true) + * [v0.26.2 for Neo4j 3.5.2](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.26.2-neo4j-3.5.2/neo4j-spatial-0.26.2-neo4j-3.5.2-server-plugin.jar?raw=true) * Using GeoTools 24.2 (for GeoServer 2.18.x): - * [v0.27.0 for Neo4j 4.0.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.0-neo4j-4.0.3/neo4j-spatial-0.27.0-neo4j-4.0.3-server-plugin.jar?raw=true) - * [v0.27.1 for Neo4j 4.1.7](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.1-neo4j-4.1.7/neo4j-spatial-0.27.1-neo4j-4.1.7-server-plugin.jar?raw=true) - * [v0.27.2 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.2-neo4j-4.2.3/neo4j-spatial-0.27.2-neo4j-4.2.3-server-plugin.jar?raw=true) - * [v0.28.0 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.28.0-neo4j-4.2.3/neo4j-spatial-0.28.0-neo4j-4.2.3-server-plugin.jar?raw=true) - * [v0.28.1 for Neo4j 4.3.10](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.28.1-neo4j-4.3.10/neo4j-spatial-0.28.1-neo4j-4.3.10-server-plugin.jar?raw=true) - * [v0.28.1 for Neo4j 4.4.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.30.0-neo4j-5.13.0/neo4j-spatial-0.30.0-neo4j-5.13.0-server-plugin.jar?raw=true) + * [v0.27.0 for Neo4j 4.0.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.0-neo4j-4.0.3/neo4j-spatial-0.27.0-neo4j-4.0.3-server-plugin.jar?raw=true) + * [v0.27.1 for Neo4j 4.1.7](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.1-neo4j-4.1.7/neo4j-spatial-0.27.1-neo4j-4.1.7-server-plugin.jar?raw=true) + * [v0.27.2 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.27.2-neo4j-4.2.3/neo4j-spatial-0.27.2-neo4j-4.2.3-server-plugin.jar?raw=true) + * [v0.28.0 for Neo4j 4.2.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.28.0-neo4j-4.2.3/neo4j-spatial-0.28.0-neo4j-4.2.3-server-plugin.jar?raw=true) + * [v0.28.1 for Neo4j 4.3.10](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.28.1-neo4j-4.3.10/neo4j-spatial-0.28.1-neo4j-4.3.10-server-plugin.jar?raw=true) + * [v0.28.1 for Neo4j 4.4.3](https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial/0.30.0-neo4j-5.13.0/neo4j-spatial-0.30.0-neo4j-5.13.0-server-plugin.jar?raw=true) For versions up to 0.15-neo4j-2.3.4: @@ -392,17 +433,23 @@ For versions for neo4j 3.0 and later: ~~~ The server plugin provides access to the internal spatial capabilities using three APIs: + * A REST API for creating layers and adding nodes or geometries to layers. - * For usage information see [Neo4j Spatial Manual REST](http://neo4j-contrib.github.io/spatial/#spatial-server-plugin) - * Note that this API provides only limited access to Spatial, with no access the the GeoPipes or import utilities - * This API was entirely removed when support for Neo4j 4.0 was added (version 0.27) + * For usage information + see [Neo4j Spatial Manual REST](http://neo4j-contrib.github.io/spatial/#spatial-server-plugin) + * Note that this API provides only limited access to Spatial, with no access the the GeoPipes or import utilities + * This API was entirely removed when support for Neo4j 4.0 was added (version 0.27) * An IndexProvider API (2.x only) for Cypher access using START node=node:geom({query}) - * It is only possible to add nodes and query for nodes, and the resulting graph structure is not compatible with any other spatial API (not compatible with Java API, REST or Procedures), so if you use this approach, do not blend it with the other approaches. - * There is some brief documentation at [Finding geometries within distance using cypher](http://neo4j-contrib.github.io/spatial/#rest-api-find-geometries-within--distance-using-cypher) - * This API was removed for 3.0 releases, and so is only available for Neo4j 2.x + * It is only possible to add nodes and query for nodes, and the resulting graph structure is not compatible with any + other spatial API (not compatible with Java API, REST or Procedures), so if you use this approach, do not blend it + with the other approaches. + * There is some brief documentation + at [Finding geometries within distance using cypher](http://neo4j-contrib.github.io/spatial/#rest-api-find-geometries-within--distance-using-cypher) + * This API was removed for 3.0 releases, and so is only available for Neo4j 2.x * Procedures for much more comprehensive access to spatial from Cypher - * Documentation is not yet available, but you can list the available procedures within Neo4j using the query `CALL spatial.procedures` - * This API uses the _Procedures_ capabilities released in Neo4j 3.0, and is therefor not available for Neo4j 2.x + * Documentation is not yet available, but you can list the available procedures within Neo4j using the + query `CALL spatial.procedures` + * This API uses the _Procedures_ capabilities released in Neo4j 3.0, and is therefor not available for Neo4j 2.x During the Neo4j 3.x releases, support for spatial procedures changed a little, through the addition of various new capabilities. @@ -412,7 +459,9 @@ making them by far the best option for accessing Neo4j remotely or through Cyphe The Java API (the original API for Neo4j Spatial) still remains, however, the most feature rich, and therefor we recommend that if you need to access Neo4j server remotely, and want deeper access to Spatial functions, consider writing your own Procedures. The Neo4j 3.0 documentation provides some good information on how to do this, -and you can also refer to the [Neo4j Spatial procedures source code](https://github.com/neo4j-contrib/spatial/blob/master/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java) for examples. +and you can also refer to +the [Neo4j Spatial procedures source code](https://github.com/neo4j-contrib/spatial/blob/master/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java) +for examples. ## Building Neo4j spatial ## @@ -481,7 +530,9 @@ Add the following repositories and dependency to your project's pom.xml: ~~~ -The version specified on the last version line can be changed to match the version you wish to work with (based on the version of Neo4j itself you are using). Too see which versions are available see the list at [Neo4j Spatial Releases](https://github.com/neo4j-contrib/m2/tree/master/releases/org/neo4j/neo4j-spatial). +The version specified on the last version line can be changed to match the version you wish to work with (based on the +version of Neo4j itself you are using). Too see which versions are available see the list +at [Neo4j Spatial Releases](https://github.com/neo4j-contrib/m2/tree/master/releases/org/neo4j/neo4j-spatial). ## Running Neo4j spatial code from the command-line ## @@ -509,7 +560,8 @@ other using the 'exec:java' target in maven. In both cases we use maven to setup _Note: On windows remember to separate the classpath with ';' instead of ':'._ -The first command above only needs to be run once, to get a copy of all required JAR files into the directory target/dependency. +The first command above only needs to be run once, to get a copy of all required JAR files into the directory +target/dependency. Once this is done, all further java commands with the -cp specifying that directory will load all dependencies. It is likely that the specific command being run does not require all dependencies copied, since it will only be using parts of the Neo4j-Spatial library, but working out exactly which dependencies are required can take a little time, so diff --git a/neo.sld.xml b/neo.sld.xml index a099c3e9c..a4088c8d9 100644 --- a/neo.sld.xml +++ b/neo.sld.xml @@ -21,9 +21,9 @@ --> + xmlns="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd"> Example Neo4j Spatial OSM Style @@ -493,7 +493,7 @@ diff --git a/pom.xml b/pom.xml index e287a296b..0d9795be7 100644 --- a/pom.xml +++ b/pom.xml @@ -1,693 +1,701 @@ - - 5.13.0 - 17 - org.neo4j.maven.skins - default-skin - 2 - 30.0 - 20100819 - 20100819 - UTF-8 - org.neo4j.gis - github - 5.10.0 - 3.2.1 - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + 5.13.0 + 17 + org.neo4j.maven.skins + default-skin + 2 + 30.0 + 20100819 + 20100819 + UTF-8 + org.neo4j.gis + github + 5.10.0 + 3.2.1 + - 4.0.0 - neo4j-spatial - org.neo4j - 0.30.0-neo4j-5.13.0 - Neo4j - Spatial Components - Spatial utilities and components for Neo4j - http://components.neo4j.org/${project.artifactId}/${project.version} - 2010 - jar - - http://github.com/neo4j/spatial/ - scm:git:git://github.com/neo4j/spatial.git - scm:git:git@github.com:neo4j/spatial.git - HEAD - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - ${neo4j.java.version} - ${neo4j.java.version} - - -AIgnoreContextWarnings - -AGeneratedDocumentationPath=target/generated-documentation - -ADocumentation.FieldDelimiter=¦ - -ADocumentation.ExportedHeaders=qualified name¦description¦signature - -ADocumentation.QuotedFields=false - -ADocumentation.DelimitedFirstField=true - -ADocumentation.ExportGrouping=PACKAGE - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - true - - - - org.apache.maven.plugins - maven-resources-plugin - 3.3.1 - - - copy-resources - - package - - copy-resources - - - ${basedir}/target/filtered-test-sources - - - ${basedir}/target/filtered-test-sources - true - - - - - - copy-docs - - package - - copy-resources - - - ${basedir}/target/docs - - - ${basedir}/src/docs - true - - - - - - - - maven-dependency-plugin - - - copy-dependencies - process-resources - - copy-dependencies - - - provided - - - - get-test-data - - unpack-dependencies - - - ${project.build.directory} - org.neo4j.spatial - - - - - - maven-assembly-plugin - 3.6.0 - - - src/main/assembly/server-plugin.xml - - - - - make-assembly - - package - - - single - - - - - - maven-surefire-plugin - ${maven-surefire-plugin.version} - - 1 - 1 - false - -server -Xms1024m -Xmx2048m - - - - org.asciidoctor - asciidoctor-maven-plugin - 2.2.3 - - - generate-docs - package - - process-asciidoc - - - - - html5 - - images - ${basedir}/target/docs - index.adoc - ${basedir}/docs - - ${project.version} - - - asciidoctor-diagram - - - - - target/docs - - **/*.txt - **/*.xml - conf - dev - examples - manual.* - docinfo.html - - - - **/*.html - **/*.png - **/*.jpg - **/*.gif - - - - coderay - style - - asciidoctor-diagram - - - - - org.asciidoctor - asciidoctorj-diagram - 2.2.1 - - - - - - - - craigtaverner - Craig Taverner - craig [at] amanzi.com - +1 - - Developer - - - - svzdvd - Davide Savazzi - davide [at] davidesavazzi.net - +1 - - Developer - - - - peterneubauer - Peter Neubauer - neubauer.peter [at] gmail.com - +1 - - Developer - - - - Andy2003 - Andreas Berger - andreas at berger-ecommerce.com - - Developer - - +1 - - - - - GNU General Public License, Version 3 - http://www.gnu.org/licenses/gpl-3.0-standalone.html - The software ("Software") developed and owned by Neo4j Sweden AB - (referred to in this notice as "Neo4j") is licensed under the GNU - GENERAL PUBLIC LICENSE Version 3 to all third parties and that license - is included below. + 4.0.0 + neo4j-spatial + org.neo4j + 0.30.0-neo4j-5.13.0 + Neo4j - Spatial Components + Spatial utilities and components for Neo4j + http://components.neo4j.org/${project.artifactId}/${project.version} + 2010 + jar + + http://github.com/neo4j/spatial/ + scm:git:git://github.com/neo4j/spatial.git + scm:git:git@github.com:neo4j/spatial.git + HEAD + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${neo4j.java.version} + ${neo4j.java.version} + + -AIgnoreContextWarnings + -AGeneratedDocumentationPath=target/generated-documentation + -ADocumentation.FieldDelimiter=¦ + -ADocumentation.ExportedHeaders=qualified name¦description¦signature + -ADocumentation.QuotedFields=false + -ADocumentation.DelimitedFirstField=true + -ADocumentation.ExportGrouping=PACKAGE + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + true + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-resources + + package + + copy-resources + + + ${basedir}/target/filtered-test-sources + + + ${basedir}/target/filtered-test-sources + true + + + + + + copy-docs + + package + + copy-resources + + + ${basedir}/target/docs + + + ${basedir}/src/docs + true + + + + + + + + maven-dependency-plugin + + + copy-dependencies + process-resources + + copy-dependencies + + + provided + + + + get-test-data + + unpack-dependencies + + + ${project.build.directory} + org.neo4j.spatial + + + + + + maven-assembly-plugin + 3.6.0 + + + src/main/assembly/server-plugin.xml + + + + + make-assembly + + package + + + single + + + + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + 1 + 1 + false + -server -Xms1024m -Xmx2048m + + + + org.asciidoctor + asciidoctor-maven-plugin + 2.2.3 + + + generate-docs + package + + process-asciidoc + + + + + html5 + + images + ${basedir}/target/docs + index.adoc + ${basedir}/docs + + ${project.version} + + + asciidoctor-diagram + + + + + target/docs + + **/*.txt + **/*.xml + conf + dev + examples + manual.* + docinfo.html + + + + **/*.html + **/*.png + **/*.jpg + **/*.gif + + + + coderay + style + + asciidoctor-diagram + + + + + org.asciidoctor + asciidoctorj-diagram + 2.2.1 + + + + + + + + craigtaverner + Craig Taverner + craig [at] amanzi.com + +1 + + Developer + + + + svzdvd + Davide Savazzi + davide [at] davidesavazzi.net + +1 + + Developer + + + + peterneubauer + Peter Neubauer + neubauer.peter [at] gmail.com + +1 + + Developer + + + + Andy2003 + Andreas Berger + andreas at berger-ecommerce.com + + Developer + + +1 + + + + + GNU General Public License, Version 3 + http://www.gnu.org/licenses/gpl-3.0-standalone.html + The software ("Software") developed and owned by Neo4j Sweden AB + (referred to in this notice as "Neo4j") is licensed under the GNU + GENERAL PUBLIC LICENSE Version 3 to all third parties and that license + is included below. - However, if you have executed an End User Software License and Services - Agreement or an OEM Software License and Support Services Agreement, or - another commercial license agreement with Neo4j or one of its - affiliates (each, a "Commercial Agreement"), the terms of the license in - such Commercial Agreement will supersede the GNU GENERAL PUBLIC LICENSE - Version 3 and you may use the Software solely pursuant to the terms of - the relevant Commercial Agreement. - - - - - - org.neo4j - neo4j - ${neo4j.version} - provided - - - com.google.code.gson - gson - 2.10.1 - test - - - org.neo4j - neo4j-graphviz - 3.1.1 - test - - - org.neo4j - neo4j-kernel - - - + However, if you have executed an End User Software License and Services + Agreement or an OEM Software License and Support Services Agreement, or + another commercial license agreement with Neo4j or one of its + affiliates (each, a "Commercial Agreement"), the terms of the license in + such Commercial Agreement will supersede the GNU GENERAL PUBLIC LICENSE + Version 3 and you may use the Software solely pursuant to the terms of + the relevant Commercial Agreement. + + + + + + org.neo4j + neo4j + ${neo4j.version} + provided + + + com.google.code.gson + gson + 2.10.1 + test + + + org.neo4j + neo4j-graphviz + 3.1.1 + test + + + org.neo4j + neo4j-kernel + + + - - - org.junit.vintage - junit-vintage-engine - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.neo4j.spatial - osm-test-data - ${spatial.test.osm.version} - test - - - org.neo4j.spatial - shp-test-data - ${spatial.test.shp.version} - test - - - org.geotools - gt-main - ${geotools.version} - - - org.geotools - gt-shapefile - ${geotools.version} - - - org.geotools - gt-process - ${geotools.version} - - - org.geotools - gt-geojson - ${geotools.version} - provided - - - org.geotools.xsd - gt-xsd-kml - ${geotools.version} - provided - - - picocontainer - picocontainer - - - - - org.geotools - gt-render - ${geotools.version} - - - it.geosolutions.imageio-ext - imageio-ext-tiff - - - - - org.neo4j.community - it-test-support - ${neo4j.version} - test - - - org.neo4j - neo4j-kernel-test-utils - ${neo4j.version} - test - - + + + org.junit.vintage + junit-vintage-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.neo4j.spatial + osm-test-data + ${spatial.test.osm.version} + test + + + org.neo4j.spatial + shp-test-data + ${spatial.test.shp.version} + test + + + org.geotools + gt-main + ${geotools.version} + + + org.geotools + gt-shapefile + ${geotools.version} + + + org.geotools + gt-process + ${geotools.version} + + + org.geotools + gt-geojson + ${geotools.version} + provided + + + org.geotools.xsd + gt-xsd-kml + ${geotools.version} + provided + + + picocontainer + picocontainer + + + + + org.geotools + gt-render + ${geotools.version} + + + it.geosolutions.imageio-ext + imageio-ext-tiff + + + + + org.neo4j.community + it-test-support + ${neo4j.version} + test + + + org.neo4j + neo4j-kernel-test-utils + ${neo4j.version} + test + + - - - - org.locationtech.jts - jts-core - 1.19.0 - - - org.slf4j - slf4j-api - 2.0.9 - - - commons-io - commons-io - 2.13.0 - - - org.apache.commons - commons-lang3 - 3.13.0 - - - jakarta.annotation - jakarta.annotation-api - 1.3.5 - - - jakarta.activation - jakarta.activation-api - 1.2.2 - - - jakarta.xml.bind - jakarta.xml.bind-api - 2.3.3 - - - org.scala-lang - scala-library - 2.13.11 - - - javax.measure - unit-api - 2.1.3 - - - org.apiguardian - apiguardian-api - 1.1.2 - - - - - - java.net - Java.net repo - https://maven.java.net/service/local/repositories/releases/content/ - - - osgeo - OSGeo Release Repository - https://repo.osgeo.org/repository/release/ - true - false - - - neo4j-contrib-releases - https://raw.github.com/neo4j-contrib/m2/master/releases - true - false - - - - - neo-docs-build - - false - - docsBuild - - - - - org.neo4j.build.plugins - neo4j-doctools - 33 - provided - - - - - - maven-dependency-plugin - - - unpack-doctools - generate-sources - - unpack-dependencies - - - jar - neo4j-doctools - ${docs.tools} - - - - - - maven-resources-plugin - - - copy-docs - process-resources - - copy-resources - - - ${project.build.directory}/docs - - - src/docs - - - - - - copy-test-sources - process-resources - - copy-resources - - - - ${project.build.directory}/test-sources/${project.artifactId}-test-sources-jar - - - - src/test/java - - - - - - copy-configuration - process-resources - - copy-resources - - - ${project.build.directory}/conf - - - src/docs/conf - - - - + + + + org.locationtech.jts + jts-core + 1.19.0 + + + org.slf4j + slf4j-api + 2.0.9 + + + commons-io + commons-io + 2.13.0 + + + org.apache.commons + commons-lang3 + 3.13.0 + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + + + jakarta.activation + jakarta.activation-api + 1.2.2 + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 + + + org.scala-lang + scala-library + 2.13.11 + + + javax.measure + unit-api + 2.1.3 + + + org.apiguardian + apiguardian-api + 1.1.2 + + + + + + java.net + Java.net repo + https://maven.java.net/service/local/repositories/releases/content/ + + + osgeo + OSGeo Release Repository + https://repo.osgeo.org/repository/release/ + + true + + + false + + + + neo4j-contrib-releases + https://raw.github.com/neo4j-contrib/m2/master/releases + + true + + + false + + + + + + neo-docs-build + + false + + docsBuild + + + + + org.neo4j.build.plugins + neo4j-doctools + 33 + provided + + + + + + maven-dependency-plugin + + + unpack-doctools + generate-sources + + unpack-dependencies + + + jar + neo4j-doctools + ${docs.tools} + + + + + + maven-resources-plugin + + + copy-docs + process-resources + + copy-resources + + + ${project.build.directory}/docs + + + src/docs + + + + + + copy-test-sources + process-resources + + copy-resources + + + + ${project.build.directory}/test-sources/${project.artifactId}-test-sources-jar + + + + src/test/java + + + + + + copy-configuration + process-resources + + copy-resources + + + ${project.build.directory}/conf + + + src/docs/conf + + + + - - filter-asciidoc-files - post-integration-test - - copy-resources - - - ${project.build.directory}/docs-filtered - - - ${project.build.directory}/docs - true - - + + filter-asciidoc-files + post-integration-test + + copy-resources + + + ${project.build.directory}/docs-filtered + + + ${project.build.directory}/docs + true + + - - - - copy-filtered-asciidoc-files-back - post-integration-test - - copy-resources - - - ${project.build.directory}/docs - - - ${project.build.directory}/docs-filtered - false - - + + + + copy-filtered-asciidoc-files-back + post-integration-test + + copy-resources + + + ${project.build.directory}/docs + + + ${project.build.directory}/docs-filtered + false + + - - - - copy-generated-files - post-integration-test - - copy-resources - - - ${project.build.directory}/src/dev - - - ${project.build.directory}/docs - - - - - - - - - - ${project.build.directory}/tools - none - - - - test-default - - - env - default - - - - default - - - - test-short - - - env - short - - - - short - - - - test-dev - - - env - dev - - - - dev - - - - test-long - - - env - long - - - - long - - - - - - - maven-javadoc-plugin - - - - GIS and Spatial - org.neo4j.gis:org.neo4j.gis.* - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - -server -Xms512m -Xmx2G -XX:+UseConcMarkSweepGC -Djava.awt.headless=true - - - - + + + + copy-generated-files + post-integration-test + + copy-resources + + + ${project.build.directory}/src/dev + + + ${project.build.directory}/docs + + + + + + + + + + ${project.build.directory}/tools + none + + + + test-default + + + env + default + + + + default + + + + test-short + + + env + short + + + + short + + + + test-dev + + + env + dev + + + + dev + + + + test-long + + + env + long + + + + long + + + + + + + maven-javadoc-plugin + + + + GIS and Spatial + org.neo4j.gis:org.neo4j.gis.* + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + -server -Xms512m -Xmx2G -XX:+UseConcMarkSweepGC -Djava.awt.headless=true + + + + - - - repo - https://raw.github.com/neo4j-contrib/m2/master/releases - - - snapshot-repo - https://raw.github.com/neo4j-contrib/m2/master/snapshots - - + + + repo + https://raw.github.com/neo4j-contrib/m2/master/releases + + + snapshot-repo + https://raw.github.com/neo4j-contrib/m2/master/snapshots + + diff --git a/src/docs/dev/export_to_gml.xml b/src/docs/dev/export_to_gml.xml index 984df38b6..c4704da91 100644 --- a/src/docs/dev/export_to_gml.xml +++ b/src/docs/dev/export_to_gml.xml @@ -1,18 +1,18 @@ - - - - 12.0,26.0 12.0,27.0 13.0,27.0 13.0,26.0 12.0,26.0 - - - + + + + 12.0,26.0 12.0,27.0 13.0,27.0 13.0,26.0 12.0,26.0 + + + - - - - 2.0,3.0 2.0,5.0 6.0,5.0 6.0,3.0 2.0,3.0 - - - - \ No newline at end of file + + + + 2.0,3.0 2.0,5.0 6.0,5.0 6.0,3.0 2.0,3.0 + + + + diff --git a/src/docs/docinfo.html b/src/docs/docinfo.html index cebb7f193..d1afac8f1 100644 --- a/src/docs/docinfo.html +++ b/src/docs/docinfo.html @@ -1,16 +1,15 @@ - - - + + - - - - - + + + + + @@ -31,12 +30,12 @@ --> - + diff --git a/src/main/assembly/docs-assembly.xml b/src/main/assembly/docs-assembly.xml index 97b28db4a..e7baf9eed 100644 --- a/src/main/assembly/docs-assembly.xml +++ b/src/main/assembly/docs-assembly.xml @@ -20,20 +20,20 @@ --> - docs - false - - jar - - - - true - target/docs - /dev - - - src/docs - / - - + docs + false + + jar + + + + true + target/docs + /dev + + + src/docs + / + + diff --git a/src/main/assembly/geoserver-plugin.xml b/src/main/assembly/geoserver-plugin.xml index 845da0019..f8a8f00f3 100644 --- a/src/main/assembly/geoserver-plugin.xml +++ b/src/main/assembly/geoserver-plugin.xml @@ -19,17 +19,19 @@ along with this program. If not, see . --> - - geoserver-plugin - - zip - - false - - - / - true - runtime - - - \ No newline at end of file + + geoserver-plugin + + zip + + false + + + / + true + runtime + + + diff --git a/src/main/assembly/server-plugin.xml b/src/main/assembly/server-plugin.xml index 4ad8f8977..77a885b4e 100644 --- a/src/main/assembly/server-plugin.xml +++ b/src/main/assembly/server-plugin.xml @@ -19,30 +19,32 @@ along with this program. If not, see . --> - - server-plugin - - jar - - false - - - / - true - true - true - runtime - - org.geotools:gt-process - org.geotools:gt-render - org.geotools:gt-coverage - javax.media:jai_imageio - - - - - - metaInf-services - - + + server-plugin + + jar + + false + + + / + true + true + true + runtime + + org.geotools:gt-process + org.geotools:gt-render + org.geotools:gt-coverage + javax.media:jai_imageio + + + + + + metaInf-services + + diff --git a/src/main/java/org/geotools/data/neo4j/DefaultResourceInfo.java b/src/main/java/org/geotools/data/neo4j/DefaultResourceInfo.java index c49207a8a..b5fd6a60f 100644 --- a/src/main/java/org/geotools/data/neo4j/DefaultResourceInfo.java +++ b/src/main/java/org/geotools/data/neo4j/DefaultResourceInfo.java @@ -22,11 +22,10 @@ import java.net.URI; import java.util.HashSet; import java.util.Set; - import org.geotools.api.data.ResourceInfo; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.feature.FeatureTypes; import org.geotools.geometry.jts.ReferencedEnvelope; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; /** * ResourceInfo implementation. @@ -42,7 +41,6 @@ public class DefaultResourceInfo implements ResourceInfo { private ReferencedEnvelope bbox; /** - * * @param name * @param crs * @param bbox @@ -59,36 +57,42 @@ public DefaultResourceInfo(String name, CoordinateReferenceSystem crs, Reference public String getName() { return name; } + /** * */ public String getTitle() { return name; } + /** * */ public String getDescription() { return description; } + /** * */ public Set getKeywords() { - return keywords; + return keywords; } + /** * */ public URI getSchema() { - return FeatureTypes.DEFAULT_NAMESPACE; + return FeatureTypes.DEFAULT_NAMESPACE; } + /** * */ public CoordinateReferenceSystem getCRS() { return crs; } + /** * */ diff --git a/src/main/java/org/geotools/data/neo4j/Neo4jFeatureBuilder.java b/src/main/java/org/geotools/data/neo4j/Neo4jFeatureBuilder.java index bd8d38f09..35aace2e9 100644 --- a/src/main/java/org/geotools/data/neo4j/Neo4jFeatureBuilder.java +++ b/src/main/java/org/geotools/data/neo4j/Neo4jFeatureBuilder.java @@ -25,23 +25,17 @@ import java.util.List; import java.util.Map; import java.util.Set; - -import org.geotools.feature.AttributeTypeBuilder; -import org.geotools.feature.simple.SimpleFeatureBuilder; -import org.geotools.feature.simple.SimpleFeatureTypeBuilder; -import org.geotools.feature.type.BasicFeatureTypes; -import org.geotools.util.Classes; -import org.neo4j.gis.spatial.Layer; -import org.neo4j.gis.spatial.SpatialDatabaseService; -import org.neo4j.gis.spatial.SpatialRecord; -import org.neo4j.graphdb.Transaction; import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.api.feature.type.AttributeDescriptor; import org.geotools.api.feature.type.GeometryDescriptor; import org.geotools.api.feature.type.GeometryType; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - +import org.geotools.feature.AttributeTypeBuilder; +import org.geotools.feature.simple.SimpleFeatureBuilder; +import org.geotools.feature.simple.SimpleFeatureTypeBuilder; +import org.geotools.feature.type.BasicFeatureTypes; +import org.geotools.util.Classes; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; @@ -50,109 +44,118 @@ import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.SpatialRecord; +import org.neo4j.graphdb.Transaction; public class Neo4jFeatureBuilder { - private static final String FEATURE_PROP_GEOM = "the_geom"; - private final SimpleFeatureBuilder builder; - private final List extraPropertyNames; + private static final String FEATURE_PROP_GEOM = "the_geom"; + private final SimpleFeatureBuilder builder; + private final List extraPropertyNames; /** * */ - public Neo4jFeatureBuilder(SimpleFeatureType sft, List extraPropertyNames) { - this.builder = new SimpleFeatureBuilder(sft); - this.extraPropertyNames = extraPropertyNames; - } + public Neo4jFeatureBuilder(SimpleFeatureType sft, List extraPropertyNames) { + this.builder = new SimpleFeatureBuilder(sft); + this.extraPropertyNames = extraPropertyNames; + } /** - * If it is necessary to lookup the layer type with a transaction, use this factory method to make the feature builder + * If it is necessary to lookup the layer type with a transaction, use this factory method to make the feature + * builder */ - public static Neo4jFeatureBuilder fromLayer(Transaction tx, Layer layer) { - return new Neo4jFeatureBuilder(getTypeFromLayer(tx, layer), Arrays.asList(layer.getExtraPropertyNames(tx))); - } - - public SimpleFeature buildFeature(String id, Geometry geometry, Map properties) { - builder.reset(); - builder.set(FEATURE_PROP_GEOM, geometry); - if (extraPropertyNames != null) { - for (String name : extraPropertyNames) { - builder.set(name, properties.get(name)); - } - } - - return builder.buildFeature(id); - } - - public SimpleFeature buildFeature(Transaction tx, SpatialRecord rec) { - return buildFeature(rec.getId(), rec.getGeometry(), rec.getProperties(tx)); - } - - public static SimpleFeatureType getTypeFromLayer(Transaction tx, Layer layer) { - return getType(layer.getName(), layer.getGeometryType(tx), layer.getCoordinateReferenceSystem(tx), layer.getExtraPropertyNames(tx)); - } - - public static SimpleFeatureType getType(String name, Integer geometryTypeId, CoordinateReferenceSystem crs, String[] extraPropertyNames) { - List types = readAttributes(geometryTypeId, crs, extraPropertyNames); - - // find Geometry type - SimpleFeatureType parent = null; - GeometryDescriptor geomDescriptor = (GeometryDescriptor) types.get(0); - Class< ? > geomBinding = geomDescriptor.getType().getBinding(); - if ((geomBinding == Point.class) || (geomBinding == MultiPoint.class)) { - parent = BasicFeatureTypes.POINT; - } else if ((geomBinding == Polygon.class) || (geomBinding == MultiPolygon.class)) { - parent = BasicFeatureTypes.POLYGON; - } else if ((geomBinding == LineString.class) || (geomBinding == MultiLineString.class) || (geomBinding == LinearRing.class)) { - parent = BasicFeatureTypes.LINE; - } - - SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); - builder.setDefaultGeometry(geomDescriptor.getLocalName()); - builder.addAll(types); - builder.setName(name); - builder.setNamespaceURI(BasicFeatureTypes.DEFAULT_NAMESPACE); - builder.setAbstract(false); - builder.setCRS(crs); - if (parent != null) { - builder.setSuperType(parent); - } - - return builder.buildFeatureType(); - } - - private static List readAttributes(Integer geometryTypeId, CoordinateReferenceSystem crs, String[] extraPropertyNames) { - Class geometryClass = SpatialDatabaseService.convertGeometryTypeToJtsClass(geometryTypeId); - - AttributeTypeBuilder build = new AttributeTypeBuilder(); - build.setName(Classes.getShortName(geometryClass)); - build.setNillable(true); - build.setCRS(crs); - build.setBinding(geometryClass); - - GeometryType geometryType = build.buildGeometryType(); - - List attributes = new ArrayList(); - attributes.add(build.buildDescriptor(BasicFeatureTypes.GEOMETRY_ATTRIBUTE_NAME, geometryType)); - - if (extraPropertyNames != null) { - Set usedNames = new HashSet(); - // record names in case of duplicates - usedNames.add(BasicFeatureTypes.GEOMETRY_ATTRIBUTE_NAME); - - for (String propertyName : extraPropertyNames) { - if (!usedNames.contains(propertyName)) { - usedNames.add(propertyName); - - build.setNillable(true); - build.setBinding(String.class); - - attributes.add(build.buildDescriptor(propertyName)); - } - } - } - - return attributes; - } + public static Neo4jFeatureBuilder fromLayer(Transaction tx, Layer layer) { + return new Neo4jFeatureBuilder(getTypeFromLayer(tx, layer), Arrays.asList(layer.getExtraPropertyNames(tx))); + } + + public SimpleFeature buildFeature(String id, Geometry geometry, Map properties) { + builder.reset(); + builder.set(FEATURE_PROP_GEOM, geometry); + if (extraPropertyNames != null) { + for (String name : extraPropertyNames) { + builder.set(name, properties.get(name)); + } + } + + return builder.buildFeature(id); + } + + public SimpleFeature buildFeature(Transaction tx, SpatialRecord rec) { + return buildFeature(rec.getId(), rec.getGeometry(), rec.getProperties(tx)); + } + + public static SimpleFeatureType getTypeFromLayer(Transaction tx, Layer layer) { + return getType(layer.getName(), layer.getGeometryType(tx), layer.getCoordinateReferenceSystem(tx), + layer.getExtraPropertyNames(tx)); + } + + public static SimpleFeatureType getType(String name, Integer geometryTypeId, CoordinateReferenceSystem crs, + String[] extraPropertyNames) { + List types = readAttributes(geometryTypeId, crs, extraPropertyNames); + + // find Geometry type + SimpleFeatureType parent = null; + GeometryDescriptor geomDescriptor = (GeometryDescriptor) types.get(0); + Class geomBinding = geomDescriptor.getType().getBinding(); + if ((geomBinding == Point.class) || (geomBinding == MultiPoint.class)) { + parent = BasicFeatureTypes.POINT; + } else if ((geomBinding == Polygon.class) || (geomBinding == MultiPolygon.class)) { + parent = BasicFeatureTypes.POLYGON; + } else if ((geomBinding == LineString.class) || (geomBinding == MultiLineString.class) || (geomBinding + == LinearRing.class)) { + parent = BasicFeatureTypes.LINE; + } + + SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); + builder.setDefaultGeometry(geomDescriptor.getLocalName()); + builder.addAll(types); + builder.setName(name); + builder.setNamespaceURI(BasicFeatureTypes.DEFAULT_NAMESPACE); + builder.setAbstract(false); + builder.setCRS(crs); + if (parent != null) { + builder.setSuperType(parent); + } + + return builder.buildFeatureType(); + } + + private static List readAttributes(Integer geometryTypeId, CoordinateReferenceSystem crs, + String[] extraPropertyNames) { + Class geometryClass = SpatialDatabaseService.convertGeometryTypeToJtsClass(geometryTypeId); + + AttributeTypeBuilder build = new AttributeTypeBuilder(); + build.setName(Classes.getShortName(geometryClass)); + build.setNillable(true); + build.setCRS(crs); + build.setBinding(geometryClass); + + GeometryType geometryType = build.buildGeometryType(); + + List attributes = new ArrayList(); + attributes.add(build.buildDescriptor(BasicFeatureTypes.GEOMETRY_ATTRIBUTE_NAME, geometryType)); + + if (extraPropertyNames != null) { + Set usedNames = new HashSet(); + // record names in case of duplicates + usedNames.add(BasicFeatureTypes.GEOMETRY_ATTRIBUTE_NAME); + + for (String propertyName : extraPropertyNames) { + if (!usedNames.contains(propertyName)) { + usedNames.add(propertyName); + + build.setNillable(true); + build.setBinding(String.class); + + attributes.add(build.buildDescriptor(propertyName)); + } + } + } + + return attributes; + } } diff --git a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStore.java b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStore.java index 3183cd617..0fb0f7bdb 100644 --- a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStore.java +++ b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStore.java @@ -19,18 +19,35 @@ */ package org.geotools.data.neo4j; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.geotools.api.data.SimpleFeatureSource; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.feature.type.Name; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.geotools.api.style.Style; +import org.geotools.api.style.StyleFactory; import org.geotools.data.store.ContentDataStore; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentFeatureSource; import org.geotools.feature.NameImpl; import org.geotools.geometry.jts.ReferencedEnvelope; -import org.geotools.api.style.Style; -import org.geotools.api.style.StyleFactory; import org.geotools.styling.StyleFactoryImpl; import org.geotools.xml.styling.SLDParser; import org.locationtech.jts.geom.Envelope; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.Constants; +import org.neo4j.gis.spatial.EditableLayer; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SpatialDatabaseRecord; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.Utilities; import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.rtree.filter.SearchAll; @@ -38,199 +55,196 @@ import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.feature.type.Name; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.*; /** * Geotools DataStore implementation. */ public class Neo4jSpatialDataStore extends ContentDataStore implements Constants { - private List typeNames; - private final Map simpleFeatureTypeIndex = Collections.synchronizedMap(new HashMap<>()); - private final Map crsIndex = Collections.synchronizedMap(new HashMap<>()); - private final Map styleIndex = Collections.synchronizedMap(new HashMap<>()); - private final Map boundsIndex = Collections.synchronizedMap(new HashMap<>()); - private final Map featureSourceIndex = Collections.synchronizedMap(new HashMap<>()); - private final GraphDatabaseService database; - private final SpatialDatabaseService spatialDatabase; - - public Neo4jSpatialDataStore(GraphDatabaseService database) { - this.database = database; - this.spatialDatabase = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); - } - - /** - * Return list of not-empty Layer names. - * The list is cached in memory. - * - * @return layer names - */ - @Override - protected List createTypeNames() { - if (typeNames == null) { - try (Transaction tx = database.beginTx()) { - List notEmptyTypes = new ArrayList<>(); - String[] allTypeNames = spatialDatabase.getLayerNames(tx); - for (String allTypeName : allTypeNames) { - // discard empty layers - System.out.print("loading layer " + allTypeName); - Layer layer = spatialDatabase.getLayer(tx, allTypeName); - if (!layer.getIndex().isEmpty(tx)) { - notEmptyTypes.add(new NameImpl(allTypeName)); - } - } - typeNames = notEmptyTypes; - tx.commit(); - } - } - return typeNames; - } - - /** - * Return FeatureType of the given Layer. - * FeatureTypes are cached in memory. - */ - public SimpleFeatureType buildFeatureType(String typeName) throws IOException { - SimpleFeatureType result = simpleFeatureTypeIndex.get(typeName); - if (result == null) { - try (Transaction tx = database.beginTx()) { - Layer layer = spatialDatabase.getLayer(tx, typeName); - if (layer == null) { - throw new IOException("Layer not found: " + typeName); - } - - result = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); - simpleFeatureTypeIndex.put(typeName, result); - tx.commit(); - } - } - - return result; - } - - public ReferencedEnvelope getBounds(String typeName) { - ReferencedEnvelope result = boundsIndex.get(typeName); - if (result == null) { - try (Transaction tx = database.beginTx()) { - Layer layer = spatialDatabase.getLayer(tx, typeName); - if (layer != null) { - Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); - result = new ReferencedEnvelope(bbox, getCRS(tx, layer)); - boundsIndex.put(typeName, result); - } - tx.commit(); - } - } - return result; - } - - public SpatialDatabaseService getSpatialDatabaseService() { - return spatialDatabase; - } - - public Transaction beginTx() { - return database.beginTx(); - } - - public void clearCache() { - typeNames = null; - simpleFeatureTypeIndex.clear(); - crsIndex.clear(); - styleIndex.clear(); - boundsIndex.clear(); - featureSourceIndex.clear(); - } - - @Override - protected ContentFeatureSource createFeatureSource(ContentEntry contentEntry) throws IOException { - Layer layer; - ArrayList records = new ArrayList<>(); - String[] extraPropertyNames; - try (Transaction tx = database.beginTx()) { - layer = spatialDatabase.getLayer(tx, contentEntry.getTypeName()); - SearchRecords results = layer.getIndex().search(tx, new SearchAll()); - // We need to pull all records during this transaction, so that later readers do not have a transaction violation - // TODO: See if there is a more memory efficient way of doing this, perhaps create a transaction at read time in the reader? - for (SpatialDatabaseRecord record : results) { - records.add(record); - } - extraPropertyNames = layer.getExtraPropertyNames(tx); - tx.commit(); - } - Neo4jSpatialFeatureSource source = new Neo4jSpatialFeatureSource(contentEntry, database, layer, buildFeatureType(contentEntry.getTypeName()), records, extraPropertyNames); - if (layer instanceof EditableLayer) { - return new Neo4jSpatialFeatureStore(contentEntry, database, (EditableLayer) layer, source); - } else { - return source; - } - } - - private CoordinateReferenceSystem getCRS(Transaction tx, Layer layer) { - CoordinateReferenceSystem result = crsIndex.get(layer.getName()); - if (result == null) { - result = layer.getCoordinateReferenceSystem(tx); - crsIndex.put(layer.getName(), result); - } - - return result; - } - - private Object getLayerStyle(String typeName) { - try (Transaction tx = database.beginTx()) { - Layer layer = spatialDatabase.getLayer(tx, typeName); - tx.commit(); - if (layer == null) return null; - else return layer.getStyle(); - } - } - - public Style getStyle(String typeName) { - Style result = styleIndex.get(typeName); - if (result == null) { - Object obj = getLayerStyle(typeName); - if (obj instanceof Style) { - result = (Style) obj; - } else if (obj instanceof File || obj instanceof String) { - StyleFactory styleFactory = new StyleFactoryImpl(); - SLDParser parser = new SLDParser(styleFactory); - try { - if (obj instanceof File) { - parser.setInput(new FileReader((File) obj)); - } else { - parser.setInput(new StringReader(obj.toString())); - } - Style[] styles = parser.readXML(); - result = styles[0]; - } catch (Exception e) { - System.err.println("Error loading style '" + obj + "': " + e.getMessage()); - e.printStackTrace(System.err); - } - } - styleIndex.put(typeName, result); - } - return result; - } - - private EditableLayer getEditableLayer(String typeName) throws IOException { - try (Transaction tx = database.beginTx()) { - Layer layer = spatialDatabase.getLayer(tx, typeName); - if (layer == null) { - throw new IOException("Layer not found: " + typeName); - } - - if (!(layer instanceof EditableLayer)) { - throw new IOException("Cannot create a FeatureWriter on a read-only layer: " + layer); - } - tx.commit(); - - return (EditableLayer) layer; - } - } + + private List typeNames; + private final Map simpleFeatureTypeIndex = Collections.synchronizedMap(new HashMap<>()); + private final Map crsIndex = Collections.synchronizedMap(new HashMap<>()); + private final Map styleIndex = Collections.synchronizedMap(new HashMap<>()); + private final Map boundsIndex = Collections.synchronizedMap(new HashMap<>()); + private final Map featureSourceIndex = Collections.synchronizedMap(new HashMap<>()); + private final GraphDatabaseService database; + private final SpatialDatabaseService spatialDatabase; + + public Neo4jSpatialDataStore(GraphDatabaseService database) { + this.database = database; + this.spatialDatabase = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); + } + + /** + * Return list of not-empty Layer names. + * The list is cached in memory. + * + * @return layer names + */ + @Override + protected List createTypeNames() { + if (typeNames == null) { + try (Transaction tx = database.beginTx()) { + List notEmptyTypes = new ArrayList<>(); + String[] allTypeNames = spatialDatabase.getLayerNames(tx); + for (String allTypeName : allTypeNames) { + // discard empty layers + System.out.print("loading layer " + allTypeName); + Layer layer = spatialDatabase.getLayer(tx, allTypeName); + if (!layer.getIndex().isEmpty(tx)) { + notEmptyTypes.add(new NameImpl(allTypeName)); + } + } + typeNames = notEmptyTypes; + tx.commit(); + } + } + return typeNames; + } + + /** + * Return FeatureType of the given Layer. + * FeatureTypes are cached in memory. + */ + public SimpleFeatureType buildFeatureType(String typeName) throws IOException { + SimpleFeatureType result = simpleFeatureTypeIndex.get(typeName); + if (result == null) { + try (Transaction tx = database.beginTx()) { + Layer layer = spatialDatabase.getLayer(tx, typeName); + if (layer == null) { + throw new IOException("Layer not found: " + typeName); + } + + result = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); + simpleFeatureTypeIndex.put(typeName, result); + tx.commit(); + } + } + + return result; + } + + public ReferencedEnvelope getBounds(String typeName) { + ReferencedEnvelope result = boundsIndex.get(typeName); + if (result == null) { + try (Transaction tx = database.beginTx()) { + Layer layer = spatialDatabase.getLayer(tx, typeName); + if (layer != null) { + Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); + result = new ReferencedEnvelope(bbox, getCRS(tx, layer)); + boundsIndex.put(typeName, result); + } + tx.commit(); + } + } + return result; + } + + public SpatialDatabaseService getSpatialDatabaseService() { + return spatialDatabase; + } + + public Transaction beginTx() { + return database.beginTx(); + } + + public void clearCache() { + typeNames = null; + simpleFeatureTypeIndex.clear(); + crsIndex.clear(); + styleIndex.clear(); + boundsIndex.clear(); + featureSourceIndex.clear(); + } + + @Override + protected ContentFeatureSource createFeatureSource(ContentEntry contentEntry) throws IOException { + Layer layer; + ArrayList records = new ArrayList<>(); + String[] extraPropertyNames; + try (Transaction tx = database.beginTx()) { + layer = spatialDatabase.getLayer(tx, contentEntry.getTypeName()); + SearchRecords results = layer.getIndex().search(tx, new SearchAll()); + // We need to pull all records during this transaction, so that later readers do not have a transaction violation + // TODO: See if there is a more memory efficient way of doing this, perhaps create a transaction at read time in the reader? + for (SpatialDatabaseRecord record : results) { + records.add(record); + } + extraPropertyNames = layer.getExtraPropertyNames(tx); + tx.commit(); + } + Neo4jSpatialFeatureSource source = new Neo4jSpatialFeatureSource(contentEntry, database, layer, + buildFeatureType(contentEntry.getTypeName()), records, extraPropertyNames); + if (layer instanceof EditableLayer) { + return new Neo4jSpatialFeatureStore(contentEntry, database, (EditableLayer) layer, source); + } else { + return source; + } + } + + private CoordinateReferenceSystem getCRS(Transaction tx, Layer layer) { + CoordinateReferenceSystem result = crsIndex.get(layer.getName()); + if (result == null) { + result = layer.getCoordinateReferenceSystem(tx); + crsIndex.put(layer.getName(), result); + } + + return result; + } + + private Object getLayerStyle(String typeName) { + try (Transaction tx = database.beginTx()) { + Layer layer = spatialDatabase.getLayer(tx, typeName); + tx.commit(); + if (layer == null) { + return null; + } else { + return layer.getStyle(); + } + } + } + + public Style getStyle(String typeName) { + Style result = styleIndex.get(typeName); + if (result == null) { + Object obj = getLayerStyle(typeName); + if (obj instanceof Style) { + result = (Style) obj; + } else if (obj instanceof File || obj instanceof String) { + StyleFactory styleFactory = new StyleFactoryImpl(); + SLDParser parser = new SLDParser(styleFactory); + try { + if (obj instanceof File) { + parser.setInput(new FileReader((File) obj)); + } else { + parser.setInput(new StringReader(obj.toString())); + } + Style[] styles = parser.readXML(); + result = styles[0]; + } catch (Exception e) { + System.err.println("Error loading style '" + obj + "': " + e.getMessage()); + e.printStackTrace(System.err); + } + } + styleIndex.put(typeName, result); + } + return result; + } + + private EditableLayer getEditableLayer(String typeName) throws IOException { + try (Transaction tx = database.beginTx()) { + Layer layer = spatialDatabase.getLayer(tx, typeName); + if (layer == null) { + throw new IOException("Layer not found: " + typeName); + } + + if (!(layer instanceof EditableLayer)) { + throw new IOException("Cannot create a FeatureWriter on a read-only layer: " + layer); + } + tx.commit(); + + return (EditableLayer) layer; + } + } } diff --git a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStoreFactory.java b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStoreFactory.java index 764583433..26576063a 100644 --- a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStoreFactory.java +++ b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialDataStoreFactory.java @@ -19,6 +19,11 @@ */ package org.geotools.data.neo4j; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.io.IOException; +import java.util.Map; import org.geotools.api.data.DataStore; import org.geotools.api.data.DataStoreFactorySpi; import org.geotools.util.KVP; @@ -26,81 +31,75 @@ import org.neo4j.dbms.api.DatabaseManagementServiceBuilder; import org.neo4j.graphdb.GraphDatabaseService; -import java.io.File; -import java.io.IOException; -import java.util.Map; - -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - /** * DataStoreFactorySpi implementation. It needs an "url" parameter containing a * path of a Neo4j neostore.id file. */ public class Neo4jSpatialDataStoreFactory implements DataStoreFactorySpi { - // TODO: This should change to Neo4j 4.x directory layout and possible multiple databases - /** - * url to the neostore.id file. - */ - public static final Param DIRECTORY = new Param("The directory path of the Neo4j database: ", File.class, - "db", true); - - public static final Param DBTYPE = new Param("dbtype", String.class, - "must be 'neo4j'", true, "neo4j", new KVP(Param.LEVEL, "program")); - - /** - * Creates a new instance of Neo4jSpatialDataStoreFactory - */ - public Neo4jSpatialDataStoreFactory() { - } - - @Override - public boolean canProcess(Map params) { - String type = (String) params.get("dbtype"); - if (type != null) { - return type.equalsIgnoreCase("neo4j"); - } - return false; - } - - @Override - public DataStore createDataStore(Map params) throws IOException { - - if (!canProcess(params)) { - throw new IOException("The parameters map isn't correct!!"); - } - - File neodir = (File) DIRECTORY.lookUp(params); - - DatabaseManagementService databases = new DatabaseManagementServiceBuilder(neodir.toPath()).build(); - GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); - - return new Neo4jSpatialDataStore(db); - } - - @Override - public DataStore createNewDataStore(Map params) throws IOException { - throw new UnsupportedOperationException("Neo4j Spatial cannot create a new database!"); - } - - @Override - public String getDisplayName() { - return "Neo4j"; - } - - @Override - public String getDescription() { - return "A datasource backed by a Neo4j Spatial database"; - } - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public Param[] getParametersInfo() { - return new Param[]{DBTYPE, DIRECTORY}; - } + // TODO: This should change to Neo4j 4.x directory layout and possible multiple databases + /** + * url to the neostore.id file. + */ + public static final Param DIRECTORY = new Param("The directory path of the Neo4j database: ", File.class, + "db", true); + + public static final Param DBTYPE = new Param("dbtype", String.class, + "must be 'neo4j'", true, "neo4j", new KVP(Param.LEVEL, "program")); + + /** + * Creates a new instance of Neo4jSpatialDataStoreFactory + */ + public Neo4jSpatialDataStoreFactory() { + } + + @Override + public boolean canProcess(Map params) { + String type = (String) params.get("dbtype"); + if (type != null) { + return type.equalsIgnoreCase("neo4j"); + } + return false; + } + + @Override + public DataStore createDataStore(Map params) throws IOException { + + if (!canProcess(params)) { + throw new IOException("The parameters map isn't correct!!"); + } + + File neodir = (File) DIRECTORY.lookUp(params); + + DatabaseManagementService databases = new DatabaseManagementServiceBuilder(neodir.toPath()).build(); + GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); + + return new Neo4jSpatialDataStore(db); + } + + @Override + public DataStore createNewDataStore(Map params) throws IOException { + throw new UnsupportedOperationException("Neo4j Spatial cannot create a new database!"); + } + + @Override + public String getDisplayName() { + return "Neo4j"; + } + + @Override + public String getDescription() { + return "A datasource backed by a Neo4j Spatial database"; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Param[] getParametersInfo() { + return new Param[]{DBTYPE, DIRECTORY}; + } } diff --git a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureSource.java b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureSource.java index ad8e3b157..bc4323de3 100644 --- a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureSource.java +++ b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureSource.java @@ -22,9 +22,11 @@ import java.io.IOException; import java.util.Iterator; import java.util.NoSuchElementException; - import org.geotools.api.data.FeatureReader; import org.geotools.api.data.Query; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentFeatureSource; import org.geotools.feature.simple.SimpleFeatureBuilder; @@ -35,9 +37,6 @@ import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; /** @@ -45,16 +44,18 @@ * Instances of this class are created by Neo4jSpatialDataStore. */ public class Neo4jSpatialFeatureSource extends ContentFeatureSource { + protected static final String FEATURE_PROP_GEOM = "the_geom"; private final GraphDatabaseService database; private final Layer layer; private final SimpleFeatureType featureType; - private final SimpleFeatureBuilder builder; + private final SimpleFeatureBuilder builder; private final Iterable results; private final String[] extraPropertyNames; - public Neo4jSpatialFeatureSource(ContentEntry contentEntry, GraphDatabaseService database, Layer layer, SimpleFeatureType featureType, Iterable results, String[] extraPropertyNames) { + public Neo4jSpatialFeatureSource(ContentEntry contentEntry, GraphDatabaseService database, Layer layer, + SimpleFeatureType featureType, Iterable results, String[] extraPropertyNames) { super(contentEntry, Query.ALL); this.database = database; this.layer = layer; @@ -98,6 +99,7 @@ protected SimpleFeatureType buildFeatureType() { } public class Reader implements FeatureReader { + private final Iterator results; Reader(Iterator results) { @@ -111,11 +113,15 @@ public SimpleFeatureType getFeatureType() { @Override public SimpleFeature next() throws IOException, IllegalArgumentException, NoSuchElementException { - if (results == null) return null; + if (results == null) { + return null; + } try (Transaction tx = database.beginTx()) { SpatialDatabaseRecord record = results.next(); - if (record == null) return null; + if (record == null) { + return null; + } record.refreshGeomNode(tx); @@ -138,7 +144,9 @@ public SimpleFeature next() throws IOException, IllegalArgumentException, NoSuch @Override public boolean hasNext() throws IOException { - if (results == null) return false; + if (results == null) { + return false; + } try (Transaction tx = database.beginTx()) { boolean ans = results.hasNext(); tx.commit(); diff --git a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureStore.java b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureStore.java index 45c21250b..3df6d18ed 100644 --- a/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureStore.java +++ b/src/main/java/org/geotools/data/neo4j/Neo4jSpatialFeatureStore.java @@ -19,9 +19,13 @@ */ package org.geotools.data.neo4j; +import java.io.IOException; +import java.util.logging.Logger; import org.geotools.api.data.FeatureReader; import org.geotools.api.data.FeatureWriter; import org.geotools.api.data.Query; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentFeatureStore; import org.geotools.feature.simple.SimpleFeatureBuilder; @@ -30,11 +34,6 @@ import org.neo4j.gis.spatial.EditableLayer; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.api.feature.simple.SimpleFeatureType; - -import java.io.IOException; -import java.util.logging.Logger; /** * FeatureWriter implementation. Instances of this class are created by @@ -42,161 +41,163 @@ */ public class Neo4jSpatialFeatureStore extends ContentFeatureStore { - private final GraphDatabaseService database; - private final SimpleFeatureType featureType; - private final Neo4jSpatialFeatureSource reader; - private final EditableLayer layer; - - private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.neo4j.gis.spatial"); - - protected Neo4jSpatialFeatureStore(ContentEntry contentEntry, GraphDatabaseService database, EditableLayer layer, Neo4jSpatialFeatureSource reader) { - super(contentEntry, Query.ALL); - this.database = database; - this.reader = reader; - this.layer = layer; - this.featureType = reader.buildFeatureType(); - } - - public SimpleFeatureType getFeatureType() { - return featureType; - } - - @Override - protected FeatureWriter getWriterInternal(Query query, int flags) { - return new Writer(reader.getReaderInternal(query)); - } - - @Override - protected ReferencedEnvelope getBoundsInternal(Query query) { - return reader.getBoundsInternal(query); - } - - @Override - protected int getCountInternal(Query query) { - return reader.getCountInternal(query); - } - - @Override - protected FeatureReader getReaderInternal(Query query) { - return reader.getReaderInternal(query); - } - - @Override - protected SimpleFeatureType buildFeatureType() { - return featureType; - } - - class Writer implements FeatureWriter { - private SimpleFeature live; // current for FeatureWriter - private SimpleFeature current; // copy of live returned to user - private boolean closed; - private final FeatureReader reader; - - public Writer(FeatureReader reader) { - this.reader = reader; - } - - @Override - public SimpleFeatureType getFeatureType() { - return reader.getFeatureType(); - } - - @Override - public SimpleFeature next() throws IOException { - if (closed) { - throw new IOException("FeatureWriter has been closed"); - } - - SimpleFeatureType featureType = getFeatureType(); - - if (hasNext()) { - live = reader.next(); - current = SimpleFeatureBuilder.copy(live); - LOGGER.finer("Calling next on writer"); - } else { - // new content - live = null; - current = SimpleFeatureBuilder.template(featureType, null); - } - - return current; - } - - @Override - public void remove() throws IOException { - if (closed) { - throw new IOException("FeatureWriter has been closed"); - } - - if (current == null) { - throw new IOException("No feature available to remove"); - } - - if (live != null) { - LOGGER.fine("Removing " + live); - - try (Transaction tx = database.beginTx()) { - layer.delete(tx, live.getID()); - tx.commit(); - } - - Neo4jSpatialFeatureStore.this.getState().fireFeatureRemoved(Neo4jSpatialFeatureStore.this, live); - } - - live = null; - current = null; - } - - @Override - public void write() throws IOException { - if (closed) { - throw new IOException("FeatureWriter has been closed"); - } - - if (current == null) { - throw new IOException("No feature available to write"); - } - - LOGGER.fine("Write called, live is " + live + " and cur is " + current); - - if (live != null) { - if (!live.equals(current)) { - LOGGER.fine("Updating " + current); - try (Transaction tx = database.beginTx()) { - layer.update(tx, current.getID(), (Geometry) current.getDefaultGeometry()); - tx.commit(); - } - - Neo4jSpatialFeatureStore.this.getState().fireFeatureUpdated( - Neo4jSpatialFeatureStore.this, live, - new ReferencedEnvelope(current.getBounds())); - - } - } else { - LOGGER.fine("Inserting " + current); - try (Transaction tx = database.beginTx()) { - layer.add(tx, (Geometry) current.getDefaultGeometry()); - tx.commit(); - } - - Neo4jSpatialFeatureStore.this.getState().fireFeatureAdded(Neo4jSpatialFeatureStore.this, current); - } - - live = null; - current = null; - } - - @Override - public boolean hasNext() throws IOException { - if (closed) { - throw new IOException("Feature writer is closed"); - } - return reader != null && reader.hasNext(); - } - - @Override - public void close() throws IOException { - reader.close(); - } - } + private final GraphDatabaseService database; + private final SimpleFeatureType featureType; + private final Neo4jSpatialFeatureSource reader; + private final EditableLayer layer; + + private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.neo4j.gis.spatial"); + + protected Neo4jSpatialFeatureStore(ContentEntry contentEntry, GraphDatabaseService database, EditableLayer layer, + Neo4jSpatialFeatureSource reader) { + super(contentEntry, Query.ALL); + this.database = database; + this.reader = reader; + this.layer = layer; + this.featureType = reader.buildFeatureType(); + } + + public SimpleFeatureType getFeatureType() { + return featureType; + } + + @Override + protected FeatureWriter getWriterInternal(Query query, int flags) { + return new Writer(reader.getReaderInternal(query)); + } + + @Override + protected ReferencedEnvelope getBoundsInternal(Query query) { + return reader.getBoundsInternal(query); + } + + @Override + protected int getCountInternal(Query query) { + return reader.getCountInternal(query); + } + + @Override + protected FeatureReader getReaderInternal(Query query) { + return reader.getReaderInternal(query); + } + + @Override + protected SimpleFeatureType buildFeatureType() { + return featureType; + } + + class Writer implements FeatureWriter { + + private SimpleFeature live; // current for FeatureWriter + private SimpleFeature current; // copy of live returned to user + private boolean closed; + private final FeatureReader reader; + + public Writer(FeatureReader reader) { + this.reader = reader; + } + + @Override + public SimpleFeatureType getFeatureType() { + return reader.getFeatureType(); + } + + @Override + public SimpleFeature next() throws IOException { + if (closed) { + throw new IOException("FeatureWriter has been closed"); + } + + SimpleFeatureType featureType = getFeatureType(); + + if (hasNext()) { + live = reader.next(); + current = SimpleFeatureBuilder.copy(live); + LOGGER.finer("Calling next on writer"); + } else { + // new content + live = null; + current = SimpleFeatureBuilder.template(featureType, null); + } + + return current; + } + + @Override + public void remove() throws IOException { + if (closed) { + throw new IOException("FeatureWriter has been closed"); + } + + if (current == null) { + throw new IOException("No feature available to remove"); + } + + if (live != null) { + LOGGER.fine("Removing " + live); + + try (Transaction tx = database.beginTx()) { + layer.delete(tx, live.getID()); + tx.commit(); + } + + Neo4jSpatialFeatureStore.this.getState().fireFeatureRemoved(Neo4jSpatialFeatureStore.this, live); + } + + live = null; + current = null; + } + + @Override + public void write() throws IOException { + if (closed) { + throw new IOException("FeatureWriter has been closed"); + } + + if (current == null) { + throw new IOException("No feature available to write"); + } + + LOGGER.fine("Write called, live is " + live + " and cur is " + current); + + if (live != null) { + if (!live.equals(current)) { + LOGGER.fine("Updating " + current); + try (Transaction tx = database.beginTx()) { + layer.update(tx, current.getID(), (Geometry) current.getDefaultGeometry()); + tx.commit(); + } + + Neo4jSpatialFeatureStore.this.getState().fireFeatureUpdated( + Neo4jSpatialFeatureStore.this, live, + new ReferencedEnvelope(current.getBounds())); + + } + } else { + LOGGER.fine("Inserting " + current); + try (Transaction tx = database.beginTx()) { + layer.add(tx, (Geometry) current.getDefaultGeometry()); + tx.commit(); + } + + Neo4jSpatialFeatureStore.this.getState().fireFeatureAdded(Neo4jSpatialFeatureStore.this, current); + } + + live = null; + current = null; + } + + @Override + public boolean hasNext() throws IOException { + if (closed) { + throw new IOException("Feature writer is closed"); + } + return reader != null && reader.hasNext(); + } + + @Override + public void close() throws IOException { + reader.close(); + } + } } diff --git a/src/main/java/org/geotools/data/neo4j/StyledImageExporter.java b/src/main/java/org/geotools/data/neo4j/StyledImageExporter.java index a48d45a43..9258c9cf9 100644 --- a/src/main/java/org/geotools/data/neo4j/StyledImageExporter.java +++ b/src/main/java/org/geotools/data/neo4j/StyledImageExporter.java @@ -19,13 +19,35 @@ */ package org.geotools.data.neo4j; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON; +import static java.util.Arrays.asList; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; import java.nio.file.Path; -import org.geotools.xml.styling.SLDParser; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.Polygon; -import org.locationtech.jts.geom.*; +import javax.imageio.ImageIO; import org.geotools.api.data.DataStore; import org.geotools.api.data.SimpleFeatureSource; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.feature.type.FeatureType; +import org.geotools.api.filter.FilterFactory; +import org.geotools.api.style.FeatureTypeStyle; +import org.geotools.api.style.Fill; +import org.geotools.api.style.Graphic; +import org.geotools.api.style.LineSymbolizer; +import org.geotools.api.style.Mark; +import org.geotools.api.style.PointSymbolizer; +import org.geotools.api.style.PolygonSymbolizer; +import org.geotools.api.style.Rule; +import org.geotools.api.style.Stroke; +import org.geotools.api.style.Style; +import org.geotools.api.style.StyleFactory; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.FeatureCollection; import org.geotools.filter.text.cql2.CQLException; @@ -34,36 +56,30 @@ import org.geotools.map.FeatureLayer; import org.geotools.map.MapContent; import org.geotools.renderer.lite.StreamingRenderer; -import org.geotools.api.style.Stroke; -import org.geotools.api.style.*; +import org.geotools.xml.styling.SLDParser; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.dbms.api.DatabaseManagementServiceBuilder; import org.neo4j.gis.spatial.SpatialTopologyUtils; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.feature.type.FeatureType; -import org.geotools.api.filter.FilterFactory; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import static java.awt.RenderingHints.*; -import static java.util.Arrays.asList; public class StyledImageExporter { + private GraphDatabaseService db; private File exportDir; double zoom = 1.0; - double[] offset = new double[] { 0, 0 }; + double[] offset = new double[]{0, 0}; Rectangle displaySize = new Rectangle(400, 300); private String[] styleFiles; static StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory(null); - static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(null); + static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(null); public StyledImageExporter(GraphDatabaseService db) { this.db = db; @@ -98,10 +114,8 @@ public Style getStyle(int i) { * window using offsets from the center, in fractions of the bounding box * dimensions. Use negative values to adjust left or down. * - * @param fractionWidth - * fraction of the width to shift right/left - * @param fractionHeight - * fraction of the height to shift up/down + * @param fractionWidth fraction of the width to shift right/left + * @param fractionHeight fraction of the height to shift up/down */ public void setOffset(double fractionWidth, double fractionHeight) { this.offset[0] = fractionWidth; @@ -121,7 +135,7 @@ private File checkFile(File file) { return file; } - @SuppressWarnings({ "unchecked", "unused" }) + @SuppressWarnings({"unchecked", "unused"}) private void debugStore(DataStore store, String[] layerNames) throws IOException { for (int i = 0; i < layerNames.length; i++) { System.out.println(asList(store.getTypeNames())); @@ -149,29 +163,34 @@ public void saveLayerImage(String layerName, String sldFile, String imageFile) t saveLayerImage(layerName, sldFile, new File(imageFile), null); } - public void saveLayerImage(String layerName, String sldFile, File imagefile, ReferencedEnvelope bounds, int width, int height, + public void saveLayerImage(String layerName, String sldFile, File imagefile, ReferencedEnvelope bounds, int width, + int height, double zoom) throws IOException { setZoom(zoom); setSize(width, height); saveLayerImage(layerName, sldFile, imagefile, bounds); } - public void saveLayerImage(String layerName, String sldFile, File imagefile, ReferencedEnvelope bounds) throws IOException { - String[] layerNames = new String[] { layerName }; + public void saveLayerImage(String layerName, String sldFile, File imagefile, ReferencedEnvelope bounds) + throws IOException { + String[] layerNames = new String[]{layerName}; saveLayerImage(layerNames, sldFile, imagefile, bounds); } - public void saveImage(FeatureCollection features, String sldFile, File imagefile) throws IOException { + public void saveImage(FeatureCollection features, String sldFile, File imagefile) + throws IOException { saveImage(features, getStyleFromSLDFile(sldFile), imagefile); } - public void saveImage(FeatureCollection features, Style style, File imagefile) throws IOException { + public void saveImage(FeatureCollection features, Style style, File imagefile) + throws IOException { MapContent mapContent = new MapContent(); mapContent.addLayer(new FeatureLayer(features, style)); saveMapContentToImageFile(mapContent, imagefile, features.getBounds()); } - public void saveImage(FeatureCollection[] features, Style[] styles, File imagefile, ReferencedEnvelope bounds) throws IOException { + public void saveImage(FeatureCollection[] features, Style[] styles, + File imagefile, ReferencedEnvelope bounds) throws IOException { MapContent mapContent = new MapContent(); for (int i = 0; i < features.length; i++) { mapContent.addLayer(new FeatureLayer(features[i], styles[i])); @@ -179,62 +198,70 @@ public void saveImage(FeatureCollection[] featu saveMapContentToImageFile(mapContent, imagefile, bounds); } - public void saveLayerImage(String[] layerNames, String sldFile, File imagefile, ReferencedEnvelope bounds) throws IOException { + public void saveLayerImage(String[] layerNames, String sldFile, File imagefile, ReferencedEnvelope bounds) + throws IOException { DataStore store = new Neo4jSpatialDataStore(db); - try (Transaction tx=db.beginTx()) { - // debugStore(store, layerNames); - StringBuffer names = new StringBuffer(); - for (String name : layerNames) { - if (names.length() > 0) - names.append(", "); - names.append(name); - } - System.out.println("Exporting layers '" + names + "' to styled image " + imagefile.getPath()); - - Style style = getStyleFromSLDFile(sldFile); - - MapContent mapContent = new MapContent(); - for (int i = 0; i < layerNames.length; i++) { - SimpleFeatureSource featureSource = store.getFeatureSource(layerNames[i]); - Style featureStyle = style; - if (featureStyle == null) { - featureStyle = getStyle(i); - } - - if (featureStyle == null) { - featureStyle = createStyleFromGeometry((SimpleFeatureType) featureSource.getSchema(), Color.BLUE, Color.CYAN); - System.out.println("Created style from geometry '" + featureSource.getSchema().getGeometryDescriptor().getType() + "': " + featureStyle); - } - - mapContent.addLayer(new org.geotools.map.FeatureLayer(featureSource, featureStyle)); - - if (bounds == null) { - bounds = featureSource.getBounds(); - } else { - bounds.expandToInclude(featureSource.getBounds()); - } - } - - saveMapContentToImageFile(mapContent, imagefile, bounds); - tx.commit(); - } + try (Transaction tx = db.beginTx()) { + // debugStore(store, layerNames); + StringBuffer names = new StringBuffer(); + for (String name : layerNames) { + if (names.length() > 0) { + names.append(", "); + } + names.append(name); + } + System.out.println("Exporting layers '" + names + "' to styled image " + imagefile.getPath()); + + Style style = getStyleFromSLDFile(sldFile); + + MapContent mapContent = new MapContent(); + for (int i = 0; i < layerNames.length; i++) { + SimpleFeatureSource featureSource = store.getFeatureSource(layerNames[i]); + Style featureStyle = style; + if (featureStyle == null) { + featureStyle = getStyle(i); + } + + if (featureStyle == null) { + featureStyle = createStyleFromGeometry((SimpleFeatureType) featureSource.getSchema(), Color.BLUE, + Color.CYAN); + System.out.println( + "Created style from geometry '" + featureSource.getSchema().getGeometryDescriptor() + .getType() + "': " + featureStyle); + } + + mapContent.addLayer(new org.geotools.map.FeatureLayer(featureSource, featureStyle)); + + if (bounds == null) { + bounds = featureSource.getBounds(); + } else { + bounds.expandToInclude(featureSource.getBounds()); + } + } + + saveMapContentToImageFile(mapContent, imagefile, bounds); + tx.commit(); + } } private Style getStyleFromSLDFile(String sldFile) { Style style = null; if (sldFile != null) { style = createStyleFromSLD(sldFile); - if (style != null) + if (style != null) { System.out.println("Created style from sldFile '" + sldFile + "': " + style); + } } return style; } - private void saveMapContentToImageFile(MapContent mapContent, File imagefile, ReferencedEnvelope bounds) throws IOException { + private void saveMapContentToImageFile(MapContent mapContent, File imagefile, ReferencedEnvelope bounds) + throws IOException { bounds = SpatialTopologyUtils.adjustBounds(bounds, 1.0 / zoom, offset); - if (displaySize == null) + if (displaySize == null) { displaySize = new Rectangle(0, 0, 800, 600); + } RenderingHints hints = new RenderingHints(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); hints.put(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON); @@ -254,54 +281,54 @@ private void saveMapContentToImageFile(MapContent mapContent, File imagefile, Re } /** - * Create a Style object from a definition in a SLD document - */ - private Style createStyleFromSLD(String sldFile) { - try { - SLDParser stylereader = new SLDParser(styleFactory, new File(sldFile).toURI().toURL()); - Style[] style = stylereader.readXML(); - return style[0]; - } catch (Exception e) { + * Create a Style object from a definition in a SLD document + */ + private Style createStyleFromSLD(String sldFile) { + try { + SLDParser stylereader = new SLDParser(styleFactory, new File(sldFile).toURI().toURL()); + Style[] style = stylereader.readXML(); + return style[0]; + } catch (Exception e) { System.err.println("Failed to read style from '" + sldFile + "': " + e.getMessage()); - } - return null; - } - - public static Style createDefaultStyle(Color strokeColor, Color fillColor) { - return createStyleFromGeometry(null, strokeColor, fillColor); - } - - /** - * Here is a programmatic alternative to using JSimpleStyleDialog to - * get a Style. This methods works out what sort of feature geometry - * we have in the shapefile and then delegates to an appropriate style - * creating method. - * TODO: Consider adding support for attribute based color schemes like in - * http://docs.geotools.org/stable/userguide/examples/stylefunctionlab.html - */ - public static Style createStyleFromGeometry(FeatureType schema, Color strokeColor, Color fillColor) { - if (schema != null) { - Class geomType = schema.getGeometryDescriptor().getType().getBinding(); - if (Polygon.class.isAssignableFrom(geomType) - || MultiPolygon.class.isAssignableFrom(geomType)) { - return createPolygonStyle(strokeColor, fillColor); - } else if (LineString.class.isAssignableFrom(geomType) - || LinearRing.class.isAssignableFrom(geomType) - || MultiLineString.class.isAssignableFrom(geomType)) { - return createLineStyle(strokeColor); - } else if (Point.class.isAssignableFrom(geomType) - || MultiPoint.class.isAssignableFrom(geomType)) { - return createPointStyle(strokeColor, fillColor); - } - } - - Style style = styleFactory.createStyle(); - style.featureTypeStyles().addAll(createPolygonStyle(strokeColor, fillColor).featureTypeStyles()); - style.featureTypeStyles().addAll(createLineStyle(strokeColor).featureTypeStyles()); - style.featureTypeStyles().addAll(createPointStyle(strokeColor, fillColor).featureTypeStyles()); + } + return null; + } + + public static Style createDefaultStyle(Color strokeColor, Color fillColor) { + return createStyleFromGeometry(null, strokeColor, fillColor); + } + + /** + * Here is a programmatic alternative to using JSimpleStyleDialog to + * get a Style. This methods works out what sort of feature geometry + * we have in the shapefile and then delegates to an appropriate style + * creating method. + * TODO: Consider adding support for attribute based color schemes like in + * http://docs.geotools.org/stable/userguide/examples/stylefunctionlab.html + */ + public static Style createStyleFromGeometry(FeatureType schema, Color strokeColor, Color fillColor) { + if (schema != null) { + Class geomType = schema.getGeometryDescriptor().getType().getBinding(); + if (Polygon.class.isAssignableFrom(geomType) + || MultiPolygon.class.isAssignableFrom(geomType)) { + return createPolygonStyle(strokeColor, fillColor); + } else if (LineString.class.isAssignableFrom(geomType) + || LinearRing.class.isAssignableFrom(geomType) + || MultiLineString.class.isAssignableFrom(geomType)) { + return createLineStyle(strokeColor); + } else if (Point.class.isAssignableFrom(geomType) + || MultiPoint.class.isAssignableFrom(geomType)) { + return createPointStyle(strokeColor, fillColor); + } + } + + Style style = styleFactory.createStyle(); + style.featureTypeStyles().addAll(createPolygonStyle(strokeColor, fillColor).featureTypeStyles()); + style.featureTypeStyles().addAll(createLineStyle(strokeColor).featureTypeStyles()); + style.featureTypeStyles().addAll(createPointStyle(strokeColor, fillColor).featureTypeStyles()); // System.out.println("Created Geometry Style: "+style); - return style; - } + return style; + } /** * Create a Style to draw polygon features @@ -313,121 +340,125 @@ public static Style createPolygonStyle(Color strokeColor, Color fillColor) { /** * Create a Style to draw polygon features */ - public static Style createPolygonStyle(Color strokeColor, Color fillColor, double stokeGamma, double fillGamma, int strokeWidth) { + public static Style createPolygonStyle(Color strokeColor, Color fillColor, double stokeGamma, double fillGamma, + int strokeWidth) { // create a partially opaque outline stroke - Stroke stroke = styleFactory.createStroke( - filterFactory.literal(strokeColor), - filterFactory.literal(strokeWidth), - filterFactory.literal(stokeGamma)); - - // create a partial opaque fill - Fill fill = styleFactory.createFill( - filterFactory.literal(fillColor), - filterFactory.literal(fillGamma)); - - /* - * Setting the geometryPropertyName arg to null signals that we want to - * draw the default geomettry of features - */ - PolygonSymbolizer sym = styleFactory.createPolygonSymbolizer(stroke, fill, null); - - Rule rule = styleFactory.createRule(); - rule.symbolizers().add(sym); - try { + Stroke stroke = styleFactory.createStroke( + filterFactory.literal(strokeColor), + filterFactory.literal(strokeWidth), + filterFactory.literal(stokeGamma)); + + // create a partial opaque fill + Fill fill = styleFactory.createFill( + filterFactory.literal(fillColor), + filterFactory.literal(fillGamma)); + + /* + * Setting the geometryPropertyName arg to null signals that we want to + * draw the default geomettry of features + */ + PolygonSymbolizer sym = styleFactory.createPolygonSymbolizer(stroke, fill, null); + + Rule rule = styleFactory.createRule(); + rule.symbolizers().add(sym); + try { rule.setFilter(ECQL.toFilter("geometryType(the_geom)='Polygon' or geometryType(the_geom)='MultiPoligon'")); } catch (CQLException e) { // TODO e.printStackTrace(); } - FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{ rule }); - Style style = styleFactory.createStyle(); - style.featureTypeStyles().add(fts); + FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule}); + Style style = styleFactory.createStyle(); + style.featureTypeStyles().add(fts); // System.out.println("Created Polygon Style: " + style); - return style; - } - - /** - * Create a Style to draw line features - */ - private static Style createLineStyle(Color strokeColor) { - Stroke stroke = styleFactory.createStroke( - filterFactory.literal(strokeColor), - filterFactory.literal(1)); - - /* - * Setting the geometryPropertyName arg to null signals that we want to - * draw the default geomettry of features - */ - LineSymbolizer sym = styleFactory.createLineSymbolizer(stroke, null); - - Rule rule = styleFactory.createRule(); - rule.symbolizers().add(sym); - try { - rule.setFilter(ECQL.toFilter("geometryType(the_geom)='LineString' or geometryType(the_geom)='LinearRing' or geometryType(the_geom)='MultiLineString'")); + return style; + } + + /** + * Create a Style to draw line features + */ + private static Style createLineStyle(Color strokeColor) { + Stroke stroke = styleFactory.createStroke( + filterFactory.literal(strokeColor), + filterFactory.literal(1)); + + /* + * Setting the geometryPropertyName arg to null signals that we want to + * draw the default geomettry of features + */ + LineSymbolizer sym = styleFactory.createLineSymbolizer(stroke, null); + + Rule rule = styleFactory.createRule(); + rule.symbolizers().add(sym); + try { + rule.setFilter(ECQL.toFilter( + "geometryType(the_geom)='LineString' or geometryType(the_geom)='LinearRing' or geometryType(the_geom)='MultiLineString'")); } catch (CQLException e) { // TODO e.printStackTrace(); } - FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule}); - Style style = styleFactory.createStyle(); - style.featureTypeStyles().add(fts); + FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule}); + Style style = styleFactory.createStyle(); + style.featureTypeStyles().add(fts); // System.out.println("Created Line Style: "+style); - return style; - } - - /** - * Create a Style to draw point features as circles with blue outlines - * and cyan fill - */ - private static Style createPointStyle(Color strokeColor, Color fillColor) { - Mark mark = styleFactory.getCircleMark(); - mark.setStroke(styleFactory.createStroke(filterFactory.literal(strokeColor), filterFactory.literal(2))); - mark.setFill(styleFactory.createFill(filterFactory.literal(fillColor))); - - Graphic gr = styleFactory.createDefaultGraphic(); - gr.graphicalSymbols().clear(); - gr.graphicalSymbols().add(mark); - gr.setSize(filterFactory.literal(5)); - - /* - * Setting the geometryPropertyName arg to null signals that we want to - * draw the default geomettry of features - */ - PointSymbolizer sym = styleFactory.createPointSymbolizer(gr, null); - - Rule rule = styleFactory.createRule(); - rule.symbolizers().add(sym); - try { + return style; + } + + /** + * Create a Style to draw point features as circles with blue outlines + * and cyan fill + */ + private static Style createPointStyle(Color strokeColor, Color fillColor) { + Mark mark = styleFactory.getCircleMark(); + mark.setStroke(styleFactory.createStroke(filterFactory.literal(strokeColor), filterFactory.literal(2))); + mark.setFill(styleFactory.createFill(filterFactory.literal(fillColor))); + + Graphic gr = styleFactory.createDefaultGraphic(); + gr.graphicalSymbols().clear(); + gr.graphicalSymbols().add(mark); + gr.setSize(filterFactory.literal(5)); + + /* + * Setting the geometryPropertyName arg to null signals that we want to + * draw the default geomettry of features + */ + PointSymbolizer sym = styleFactory.createPointSymbolizer(gr, null); + + Rule rule = styleFactory.createRule(); + rule.symbolizers().add(sym); + try { rule.setFilter(ECQL.toFilter("geometryType(the_geom)='Point' or geometryType(the_geom)='MultiPoint'")); } catch (CQLException e) { // TODO e.printStackTrace(); } - FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule}); - Style style = styleFactory.createStyle(); - style.featureTypeStyles().add(fts); + FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule}); + Style style = styleFactory.createStyle(); + style.featureTypeStyles().add(fts); // System.out.println("Created Point Style: " + style); - return style; - } + return style; + } - public static void main(String[] args) { + public static void main(String[] args) { if (args.length < 4) { - System.err.println("Too few arguments. Provide: 'homeDir', 'database' 'exportdir' 'stylefile' zoom layer "); - System.err.println("\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); + System.err.println( + "Too few arguments. Provide: 'homeDir', 'database' 'exportdir' 'stylefile' zoom layer "); + System.err.println( + "\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); System.exit(1); } String homeDir = args[0]; String database = args[1]; String exportdir = args[2]; String stylefile = args[3]; - double zoom = Double.valueOf(args[4]); - DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(homeDir)).build(); + double zoom = Double.valueOf(args[4]); + DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(homeDir)).build(); GraphDatabaseService db = databases.database(database); try { StyledImageExporter imageExporter = new StyledImageExporter(db); diff --git a/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java index 38fcb1cd6..5c00f3ec6 100644 --- a/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/AbstractGeometryEncoder.java @@ -20,111 +20,109 @@ package org.neo4j.gis.spatial; import org.apache.commons.lang3.ArrayUtils; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.rtree.Envelope; -import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Entity; - -import org.locationtech.jts.geom.Geometry; +import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; public abstract class AbstractGeometryEncoder implements GeometryEncoder, Constants { - protected String bboxProperty = PROP_BBOX; - - // Public methods - - @Override - public void init(Layer layer) { - this.layer = layer; - } - - public void encodeEnvelope(Envelope mbb, Entity container) { - container.setProperty(bboxProperty, new double[]{mbb.getMinX(), mbb.getMinY(), mbb.getMaxX(), mbb.getMaxY()}); - } - - @Override - public void ensureIndexable(Geometry geometry, Entity container) { - container.setProperty(PROP_TYPE, encodeGeometryType(geometry.getGeometryType())); - encodeEnvelope(Utilities.fromJtsToNeo4j(geometry.getEnvelopeInternal()), container); - } - - @Override - public void encodeGeometry(Transaction tx, Geometry geometry, Entity container) { - ensureIndexable(geometry, container); - encodeGeometryShape(tx, geometry, container); - } - - @Override - public Envelope decodeEnvelope(Entity container) { - double[] bbox = new double[]{0, 0, 0, 0}; - Object bboxProp = container.getProperty(bboxProperty); - if (bboxProp instanceof Double[]) { - bbox = ArrayUtils.toPrimitive((Double[]) bboxProp); - } else if (bboxProp instanceof double[]) { - bbox = (double[]) bboxProp; - } - - // Envelope parameters: xmin, xmax, ymin, ymax - return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); - } - - - // Protected methods - - protected abstract void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container); - - protected Integer encodeGeometryType(String jtsGeometryType) { - // TODO: Consider alternatives for specifying type, like relationship to - // type category - // objects (or similar indexing structure) - if ("Point".equals(jtsGeometryType)) { - return GTYPE_POINT; - } else if ("MultiPoint".equals(jtsGeometryType)) { - return GTYPE_MULTIPOINT; - } else if ("LineString".equals(jtsGeometryType)) { - return GTYPE_LINESTRING; - } else if ("MultiLineString".equals(jtsGeometryType)) { - return GTYPE_MULTILINESTRING; - } else if ("Polygon".equals(jtsGeometryType)) { - return GTYPE_POLYGON; - } else if ("MultiPolygon".equals(jtsGeometryType)) { - return GTYPE_MULTIPOLYGON; - } else { - throw new IllegalArgumentException("unknown type:" + jtsGeometryType); - } - } - - /** - * This method wraps the hasProperty(String) method on the geometry node. - * This means the default way of storing attributes is simply as properties - * of the geometry node. This behaviour can be changed by other domain - * models with different encodings. - */ - public boolean hasAttribute(Node geomNode, String name) { - return geomNode.hasProperty(name); - } - - /** - * This method wraps the getProperty(String,null) method on the geometry - * node. This means the default way of storing attributes is simply as - * properties of the geometry node. This behaviour can be changed by other - * domain models with different encodings. If the property does not exist, - * the method returns null. - */ - public Object getAttribute(Node geomNode, String name) { - return geomNode.getProperty(name, null); - } - - /** - * For external expression of the configuration of this geometry encoder - * - * @return descriptive signature of encoder, type and configuration - */ - public String getSignature() { - return "GeometryEncoder(bbox='" + bboxProperty + "')"; - } - - // Attributes - - protected Layer layer; -} \ No newline at end of file + protected String bboxProperty = PROP_BBOX; + + // Public methods + + @Override + public void init(Layer layer) { + this.layer = layer; + } + + public void encodeEnvelope(Envelope mbb, Entity container) { + container.setProperty(bboxProperty, new double[]{mbb.getMinX(), mbb.getMinY(), mbb.getMaxX(), mbb.getMaxY()}); + } + + @Override + public void ensureIndexable(Geometry geometry, Entity container) { + container.setProperty(PROP_TYPE, encodeGeometryType(geometry.getGeometryType())); + encodeEnvelope(Utilities.fromJtsToNeo4j(geometry.getEnvelopeInternal()), container); + } + + @Override + public void encodeGeometry(Transaction tx, Geometry geometry, Entity container) { + ensureIndexable(geometry, container); + encodeGeometryShape(tx, geometry, container); + } + + @Override + public Envelope decodeEnvelope(Entity container) { + double[] bbox = new double[]{0, 0, 0, 0}; + Object bboxProp = container.getProperty(bboxProperty); + if (bboxProp instanceof Double[]) { + bbox = ArrayUtils.toPrimitive((Double[]) bboxProp); + } else if (bboxProp instanceof double[]) { + bbox = (double[]) bboxProp; + } + + // Envelope parameters: xmin, xmax, ymin, ymax + return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); + } + + // Protected methods + + protected abstract void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container); + + protected Integer encodeGeometryType(String jtsGeometryType) { + // TODO: Consider alternatives for specifying type, like relationship to + // type category + // objects (or similar indexing structure) + if ("Point".equals(jtsGeometryType)) { + return GTYPE_POINT; + } else if ("MultiPoint".equals(jtsGeometryType)) { + return GTYPE_MULTIPOINT; + } else if ("LineString".equals(jtsGeometryType)) { + return GTYPE_LINESTRING; + } else if ("MultiLineString".equals(jtsGeometryType)) { + return GTYPE_MULTILINESTRING; + } else if ("Polygon".equals(jtsGeometryType)) { + return GTYPE_POLYGON; + } else if ("MultiPolygon".equals(jtsGeometryType)) { + return GTYPE_MULTIPOLYGON; + } else { + throw new IllegalArgumentException("unknown type:" + jtsGeometryType); + } + } + + /** + * This method wraps the hasProperty(String) method on the geometry node. + * This means the default way of storing attributes is simply as properties + * of the geometry node. This behaviour can be changed by other domain + * models with different encodings. + */ + public boolean hasAttribute(Node geomNode, String name) { + return geomNode.hasProperty(name); + } + + /** + * This method wraps the getProperty(String,null) method on the geometry + * node. This means the default way of storing attributes is simply as + * properties of the geometry node. This behaviour can be changed by other + * domain models with different encodings. If the property does not exist, + * the method returns null. + */ + public Object getAttribute(Node geomNode, String name) { + return geomNode.getProperty(name, null); + } + + /** + * For external expression of the configuration of this geometry encoder + * + * @return descriptive signature of encoder, type and configuration + */ + public String getSignature() { + return "GeometryEncoder(bbox='" + bboxProperty + "')"; + } + + // Attributes + + protected Layer layer; +} diff --git a/src/main/java/org/neo4j/gis/spatial/ConsoleListener.java b/src/main/java/org/neo4j/gis/spatial/ConsoleListener.java index bb32d7022..8007d4fab 100644 --- a/src/main/java/org/neo4j/gis/spatial/ConsoleListener.java +++ b/src/main/java/org/neo4j/gis/spatial/ConsoleListener.java @@ -20,16 +20,16 @@ package org.neo4j.gis.spatial; import java.io.PrintStream; - import org.neo4j.gis.spatial.rtree.Listener; /** * This listener simply logs progress to System.out. - * + * * @author Craig Taverner */ public class ConsoleListener implements Listener { + private PrintStream out; private int total = 0; private int current = 0; diff --git a/src/main/java/org/neo4j/gis/spatial/Constants.java b/src/main/java/org/neo4j/gis/spatial/Constants.java index 19113b412..6013c41ec 100644 --- a/src/main/java/org/neo4j/gis/spatial/Constants.java +++ b/src/main/java/org/neo4j/gis/spatial/Constants.java @@ -26,47 +26,47 @@ */ public interface Constants { - // Node properties + // Node properties - String PROP_BBOX = "bbox"; - String PROP_LAYER = "layer"; - String PROP_LAYERNODEEXTRAPROPS = "layerprops"; - String PROP_CRS = "layercrs"; - String PROP_CREATIONTIME = "ctime"; - String PROP_GEOMENCODER = "geomencoder"; - String PROP_INDEX_CLASS = "index_class"; - String PROP_GEOMENCODER_CONFIG = "geomencoder_config"; - String PROP_INDEX_CONFIG = "index_config"; - String PROP_LAYER_CLASS = "layer_class"; + String PROP_BBOX = "bbox"; + String PROP_LAYER = "layer"; + String PROP_LAYERNODEEXTRAPROPS = "layerprops"; + String PROP_CRS = "layercrs"; + String PROP_CREATIONTIME = "ctime"; + String PROP_GEOMENCODER = "geomencoder"; + String PROP_INDEX_CLASS = "index_class"; + String PROP_GEOMENCODER_CONFIG = "geomencoder_config"; + String PROP_INDEX_CONFIG = "index_config"; + String PROP_LAYER_CLASS = "layer_class"; - String PROP_TYPE = "gtype"; - String PROP_QUERY = "query"; - String PROP_WKB = "wkb"; - String PROP_WKT = "wkt"; - String PROP_GEOM = "geometry"; + String PROP_TYPE = "gtype"; + String PROP_QUERY = "query"; + String PROP_WKB = "wkb"; + String PROP_WKT = "wkt"; + String PROP_GEOM = "geometry"; - String[] RESERVED_PROPS = new String[]{ - PROP_BBOX, - PROP_LAYER, - PROP_LAYERNODEEXTRAPROPS, - PROP_CRS, - PROP_CREATIONTIME, - PROP_TYPE, - PROP_WKB, - PROP_WKT, - PROP_GEOM - }; + String[] RESERVED_PROPS = new String[]{ + PROP_BBOX, + PROP_LAYER, + PROP_LAYERNODEEXTRAPROPS, + PROP_CRS, + PROP_CREATIONTIME, + PROP_TYPE, + PROP_WKB, + PROP_WKT, + PROP_GEOM + }; - Label LABEL_LAYER = Label.label("SpatialLayer"); + Label LABEL_LAYER = Label.label("SpatialLayer"); - // OpenGIS geometry type numbers + // OpenGIS geometry type numbers - int GTYPE_GEOMETRY = 0; - int GTYPE_POINT = 1; - int GTYPE_LINESTRING = 2; - int GTYPE_POLYGON = 3; - int GTYPE_MULTIPOINT = 4; - int GTYPE_MULTILINESTRING = 5; - int GTYPE_MULTIPOLYGON = 6; + int GTYPE_GEOMETRY = 0; + int GTYPE_POINT = 1; + int GTYPE_LINESTRING = 2; + int GTYPE_POLYGON = 3; + int GTYPE_MULTIPOINT = 4; + int GTYPE_MULTILINESTRING = 5; + int GTYPE_MULTIPOLYGON = 6; -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/DefaultLayer.java b/src/main/java/org/neo4j/gis/spatial/DefaultLayer.java index cab742c0f..bdee96bf3 100644 --- a/src/main/java/org/neo4j/gis/spatial/DefaultLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/DefaultLayer.java @@ -19,6 +19,12 @@ */ package org.neo4j.gis.spatial; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.PrecisionModel; @@ -34,9 +40,6 @@ import org.neo4j.gis.spatial.utilities.GeotoolsAdapter; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.util.*; /** * Instances of Layer provide the ability for developers to add/remove and edit @@ -51,329 +54,332 @@ */ public class DefaultLayer implements Constants, Layer, SpatialDataset { - // Public methods - - public String getName() { - return name; - } - - public LayerIndexReader getIndex() { - return indexReader; - } - - public String getSignature() { - return "Layer(name='" + getName() + "', encoder=" + getGeometryEncoder().getSignature() + ")"; - } - - /** - * Add the geometry encoded in the given Node. This causes the geometry to appear in the index. - */ - public SpatialDatabaseRecord add(Transaction tx, Node geomNode) { - Geometry geometry = getGeometryEncoder().decodeGeometry(geomNode); - - // add BBOX to Node if it's missing - getGeometryEncoder().ensureIndexable(geometry, geomNode); - - indexWriter.add(tx, geomNode); - return new SpatialDatabaseRecord(this, geomNode, geometry); - } - - @Override - public int addAll(Transaction tx, List geomNodes) { - GeometryEncoder geometryEncoder = getGeometryEncoder(); - - for (Node geomNode : geomNodes) { - Geometry geometry = geometryEncoder.decodeGeometry(geomNode); - // add BBOX to Node if it's missing - geometryEncoder.encodeGeometry(tx, geometry, geomNode); - } - indexWriter.add(tx, geomNodes); - return geomNodes.size(); - } - - public GeometryFactory getGeometryFactory() { - return geometryFactory; - } - - public void setCoordinateReferenceSystem(Transaction tx, CoordinateReferenceSystem crs) { - Node layerNode = getLayerNode(tx); - layerNode.setProperty(PROP_CRS, crs.toWKT()); - } - - public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { - Node layerNode = getLayerNode(tx); - if (layerNode.hasProperty(PROP_CRS)) { - return GeotoolsAdapter.getCRS((String) layerNode.getProperty(PROP_CRS)); - } else { - return null; - } - } - - public void setGeometryType(Transaction tx, int geometryType) { - Node layerNode = getLayerNode(tx); - if (geometryType < GTYPE_POINT || geometryType > GTYPE_MULTIPOLYGON) { - throw new IllegalArgumentException("Unknown geometry type: " + geometryType); - } - - layerNode.setProperty(PROP_TYPE, geometryType); - } - - public Integer getGeometryType(Transaction tx) { - Node layerNode = getLayerNode(tx); - if (layerNode.hasProperty(PROP_TYPE)) { - return (Integer) layerNode.getProperty(PROP_TYPE); - } else { - GuessGeometryTypeSearch geomTypeSearch = new GuessGeometryTypeSearch(); - indexReader.searchIndex(tx, geomTypeSearch).count(); - - // returns null for an empty layer! - return geomTypeSearch.firstFoundType; - } - } - - private static class GuessGeometryTypeSearch implements SearchFilter { - - Integer firstFoundType; - - @Override - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return firstFoundType == null; - } - - @Override - public boolean geometryMatches(Transaction tx, Node geomNode) { - if (firstFoundType == null) { - firstFoundType = (Integer) geomNode.getProperty(PROP_TYPE); - } - - return false; - } - } - - public String[] getExtraPropertyNames(Transaction tx) { - Node layerNode = getLayerNode(tx); - String[] extraPropertyNames; - if (layerNode.hasProperty(PROP_LAYERNODEEXTRAPROPS)) { - extraPropertyNames = (String[]) layerNode.getProperty(PROP_LAYERNODEEXTRAPROPS); - } else { - extraPropertyNames = new String[]{}; - } - return extraPropertyNames; - } - - public void setExtraPropertyNames(String[] names, Transaction tx) { - getLayerNode(tx).setProperty(PROP_LAYERNODEEXTRAPROPS, names); - } - - void mergeExtraPropertyNames(Transaction tx, String[] names) { - Node layerNode = getLayerNode(tx); - if (layerNode.hasProperty(PROP_LAYERNODEEXTRAPROPS)) { - String[] actualNames = (String[]) layerNode.getProperty(PROP_LAYERNODEEXTRAPROPS); - - Set mergedNames = new HashSet<>(); - Collections.addAll(mergedNames, names); - Collections.addAll(mergedNames, actualNames); - - layerNode.setProperty(PROP_LAYERNODEEXTRAPROPS, mergedNames.toArray(new String[0])); - } else { - layerNode.setProperty(PROP_LAYERNODEEXTRAPROPS, names); - } - } - - /** - * The constructor is protected because we should not construct this class - * directly, but use the factory methods to create Layers based on - * configurations - */ - protected DefaultLayer() { - } - - @Override - public void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode) { - //this.spatialDatabase = spatialDatabase; - this.name = name; - this.layerNodeId = layerNode.getElementId(); - - this.geometryFactory = new GeometryFactory(); - CoordinateReferenceSystem crs = getCoordinateReferenceSystem(tx); - if (crs != null) { - // TODO: Verify this code works for general cases to read SRID from layer properties and use them to construct GeometryFactory - Integer code = GeotoolsAdapter.getEPSGCode(crs); - if (code != null) { - this.geometryFactory = new GeometryFactory(new PrecisionModel(), code); - } - } - - if (layerNode.hasProperty(PROP_GEOMENCODER)) { - String encoderClassName = (String) layerNode.getProperty(PROP_GEOMENCODER); - try { - this.geometryEncoder = (GeometryEncoder) Class.forName(encoderClassName).getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new SpatialDatabaseException(e); - } - if (this.geometryEncoder instanceof Configurable) { - if (layerNode.hasProperty(PROP_GEOMENCODER_CONFIG)) { - ((Configurable) this.geometryEncoder).setConfiguration((String) layerNode.getProperty(PROP_GEOMENCODER_CONFIG)); - } - } - } else { - this.geometryEncoder = new WKBGeometryEncoder(); - } - this.geometryEncoder.init(this); - - // index must be created *after* geometryEncoder - if (layerNode.hasProperty(PROP_INDEX_CLASS)) { - String indexClass = (String) layerNode.getProperty(PROP_INDEX_CLASS); - try { - Object index = Class.forName(indexClass).getDeclaredConstructor().newInstance(); - this.indexReader = (LayerIndexReader) index; - this.indexWriter = (SpatialIndexWriter) index; - } catch (Exception e) { - throw new SpatialDatabaseException(e); - } - if (this.indexReader instanceof Configurable) { - if (layerNode.hasProperty(PROP_INDEX_CONFIG)) { - ((Configurable) this.indexReader).setConfiguration((String) layerNode.getProperty(PROP_INDEX_CONFIG)); - } - } - } else { - LayerRTreeIndex index = new LayerRTreeIndex(); - this.indexReader = index; - this.indexWriter = index; - } - this.indexReader.init(tx, indexManager, this); - } - - /** - * All layers are associated with a single node in the database. This node will have properties, - * relationships (sub-graph) or both to describe the contents of the layer - */ - public Node getLayerNode(Transaction tx) { - return tx.getNodeByElementId(layerNodeId); - } - - /** - * Delete Layer - */ - public void delete(Transaction tx, Listener monitor) { - indexWriter.removeAll(tx, true, monitor); - Node layerNode = getLayerNode(tx); - layerNode.delete(); - layerNodeId = null; - } - - // Private methods + // Public methods + + public String getName() { + return name; + } + + public LayerIndexReader getIndex() { + return indexReader; + } + + public String getSignature() { + return "Layer(name='" + getName() + "', encoder=" + getGeometryEncoder().getSignature() + ")"; + } + + /** + * Add the geometry encoded in the given Node. This causes the geometry to appear in the index. + */ + public SpatialDatabaseRecord add(Transaction tx, Node geomNode) { + Geometry geometry = getGeometryEncoder().decodeGeometry(geomNode); + + // add BBOX to Node if it's missing + getGeometryEncoder().ensureIndexable(geometry, geomNode); + + indexWriter.add(tx, geomNode); + return new SpatialDatabaseRecord(this, geomNode, geometry); + } + + @Override + public int addAll(Transaction tx, List geomNodes) { + GeometryEncoder geometryEncoder = getGeometryEncoder(); + + for (Node geomNode : geomNodes) { + Geometry geometry = geometryEncoder.decodeGeometry(geomNode); + // add BBOX to Node if it's missing + geometryEncoder.encodeGeometry(tx, geometry, geomNode); + } + indexWriter.add(tx, geomNodes); + return geomNodes.size(); + } + + public GeometryFactory getGeometryFactory() { + return geometryFactory; + } + + public void setCoordinateReferenceSystem(Transaction tx, CoordinateReferenceSystem crs) { + Node layerNode = getLayerNode(tx); + layerNode.setProperty(PROP_CRS, crs.toWKT()); + } + + public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { + Node layerNode = getLayerNode(tx); + if (layerNode.hasProperty(PROP_CRS)) { + return GeotoolsAdapter.getCRS((String) layerNode.getProperty(PROP_CRS)); + } else { + return null; + } + } + + public void setGeometryType(Transaction tx, int geometryType) { + Node layerNode = getLayerNode(tx); + if (geometryType < GTYPE_POINT || geometryType > GTYPE_MULTIPOLYGON) { + throw new IllegalArgumentException("Unknown geometry type: " + geometryType); + } + + layerNode.setProperty(PROP_TYPE, geometryType); + } + + public Integer getGeometryType(Transaction tx) { + Node layerNode = getLayerNode(tx); + if (layerNode.hasProperty(PROP_TYPE)) { + return (Integer) layerNode.getProperty(PROP_TYPE); + } else { + GuessGeometryTypeSearch geomTypeSearch = new GuessGeometryTypeSearch(); + indexReader.searchIndex(tx, geomTypeSearch).count(); + + // returns null for an empty layer! + return geomTypeSearch.firstFoundType; + } + } + + private static class GuessGeometryTypeSearch implements SearchFilter { + + Integer firstFoundType; + + @Override + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return firstFoundType == null; + } + + @Override + public boolean geometryMatches(Transaction tx, Node geomNode) { + if (firstFoundType == null) { + firstFoundType = (Integer) geomNode.getProperty(PROP_TYPE); + } + + return false; + } + } + + public String[] getExtraPropertyNames(Transaction tx) { + Node layerNode = getLayerNode(tx); + String[] extraPropertyNames; + if (layerNode.hasProperty(PROP_LAYERNODEEXTRAPROPS)) { + extraPropertyNames = (String[]) layerNode.getProperty(PROP_LAYERNODEEXTRAPROPS); + } else { + extraPropertyNames = new String[]{}; + } + return extraPropertyNames; + } + + public void setExtraPropertyNames(String[] names, Transaction tx) { + getLayerNode(tx).setProperty(PROP_LAYERNODEEXTRAPROPS, names); + } + + void mergeExtraPropertyNames(Transaction tx, String[] names) { + Node layerNode = getLayerNode(tx); + if (layerNode.hasProperty(PROP_LAYERNODEEXTRAPROPS)) { + String[] actualNames = (String[]) layerNode.getProperty(PROP_LAYERNODEEXTRAPROPS); + + Set mergedNames = new HashSet<>(); + Collections.addAll(mergedNames, names); + Collections.addAll(mergedNames, actualNames); + + layerNode.setProperty(PROP_LAYERNODEEXTRAPROPS, mergedNames.toArray(new String[0])); + } else { + layerNode.setProperty(PROP_LAYERNODEEXTRAPROPS, names); + } + } + + /** + * The constructor is protected because we should not construct this class + * directly, but use the factory methods to create Layers based on + * configurations + */ + protected DefaultLayer() { + } + + @Override + public void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode) { + //this.spatialDatabase = spatialDatabase; + this.name = name; + this.layerNodeId = layerNode.getElementId(); + + this.geometryFactory = new GeometryFactory(); + CoordinateReferenceSystem crs = getCoordinateReferenceSystem(tx); + if (crs != null) { + // TODO: Verify this code works for general cases to read SRID from layer properties and use them to construct GeometryFactory + Integer code = GeotoolsAdapter.getEPSGCode(crs); + if (code != null) { + this.geometryFactory = new GeometryFactory(new PrecisionModel(), code); + } + } + + if (layerNode.hasProperty(PROP_GEOMENCODER)) { + String encoderClassName = (String) layerNode.getProperty(PROP_GEOMENCODER); + try { + this.geometryEncoder = (GeometryEncoder) Class.forName(encoderClassName).getDeclaredConstructor() + .newInstance(); + } catch (Exception e) { + throw new SpatialDatabaseException(e); + } + if (this.geometryEncoder instanceof Configurable) { + if (layerNode.hasProperty(PROP_GEOMENCODER_CONFIG)) { + ((Configurable) this.geometryEncoder).setConfiguration( + (String) layerNode.getProperty(PROP_GEOMENCODER_CONFIG)); + } + } + } else { + this.geometryEncoder = new WKBGeometryEncoder(); + } + this.geometryEncoder.init(this); + + // index must be created *after* geometryEncoder + if (layerNode.hasProperty(PROP_INDEX_CLASS)) { + String indexClass = (String) layerNode.getProperty(PROP_INDEX_CLASS); + try { + Object index = Class.forName(indexClass).getDeclaredConstructor().newInstance(); + this.indexReader = (LayerIndexReader) index; + this.indexWriter = (SpatialIndexWriter) index; + } catch (Exception e) { + throw new SpatialDatabaseException(e); + } + if (this.indexReader instanceof Configurable) { + if (layerNode.hasProperty(PROP_INDEX_CONFIG)) { + ((Configurable) this.indexReader).setConfiguration( + (String) layerNode.getProperty(PROP_INDEX_CONFIG)); + } + } + } else { + LayerRTreeIndex index = new LayerRTreeIndex(); + this.indexReader = index; + this.indexWriter = index; + } + this.indexReader.init(tx, indexManager, this); + } + + /** + * All layers are associated with a single node in the database. This node will have properties, + * relationships (sub-graph) or both to describe the contents of the layer + */ + public Node getLayerNode(Transaction tx) { + return tx.getNodeByElementId(layerNodeId); + } + + /** + * Delete Layer + */ + public void delete(Transaction tx, Listener monitor) { + indexWriter.removeAll(tx, true, monitor); + Node layerNode = getLayerNode(tx); + layerNode.delete(); + layerNodeId = null; + } + + // Private methods // protected GraphDatabaseService getDatabase() { // return spatialDatabase.getDatabase(); // } - - // Attributes - - //private SpatialDatabaseService spatialDatabase; - private String name; - protected String layerNodeId = null; - private GeometryEncoder geometryEncoder; - private GeometryFactory geometryFactory; - protected LayerIndexReader indexReader; - protected SpatialIndexWriter indexWriter; - - public SpatialDataset getDataset() { - return this; - } - - public Iterable getAllGeometryNodes(Transaction tx) { - return indexReader.getAllIndexedNodes(tx); - } - - @Override - public boolean containsGeometryNode(Transaction tx, Node geomNode) { - return indexReader.isNodeIndexed(tx, geomNode.getElementId()); - } - - /** - * Provides a method for iterating over all geometries in this dataset. This is similar to the - * getAllGeometryNodes() method but internally converts the Node to a Geometry. - * - * @return iterable over geometries in the dataset - */ - public Iterable getAllGeometries(Transaction tx) { - return new NodeToGeometryIterable(getAllGeometryNodes(tx)); - } - - /** - * In order to wrap one iterable or iterator in another that converts the objects from one type - * to another without loading all into memory, we need to use this ugly java-magic. Man, I miss - * Ruby right now! - */ - private class NodeToGeometryIterable implements Iterable { - private final Iterator allGeometryNodeIterator; - - private class GeometryIterator implements Iterator { - - public boolean hasNext() { - return NodeToGeometryIterable.this.allGeometryNodeIterator.hasNext(); - } - - public Geometry next() { - return geometryEncoder.decodeGeometry(NodeToGeometryIterable.this.allGeometryNodeIterator.next()); - } - - public void remove() { - } - - } - - NodeToGeometryIterable(Iterable allGeometryNodes) { - this.allGeometryNodeIterator = allGeometryNodes.iterator(); - } - - public Iterator iterator() { - return new GeometryIterator(); - } - - } - - /** - * Return the geometry encoder used by this SpatialDataset to convert individual geometries to - * and from the database structure. - * - * @return GeometryEncoder for this dataset - */ - public GeometryEncoder getGeometryEncoder() { - return geometryEncoder; - } - - /** - * This dataset contains only one layer, itself. - * - * @return iterable over all Layers that can be viewed from this dataset - */ - public Iterable getLayers() { - return Collections.singletonList(this); - } - - /** - * Override this method to provide a style if your layer wishes to control - * its own rendering in the GIS. If a Style is returned, it is used. If a - * File is returned, it is opened and assumed to contain SLD contents. If a - * String is returned, it is assumed to contain SLD contents. - * - * @return null - */ - public Object getStyle() { - return null; - } - - private PropertyMappingManager propertyMappingManager; - - @Override - public PropertyMappingManager getPropertyMappingManager() { - if (propertyMappingManager == null) { - propertyMappingManager = new PropertyMappingManager(this); - } - return propertyMappingManager; - } + // Attributes + + //private SpatialDatabaseService spatialDatabase; + private String name; + protected String layerNodeId = null; + private GeometryEncoder geometryEncoder; + private GeometryFactory geometryFactory; + protected LayerIndexReader indexReader; + protected SpatialIndexWriter indexWriter; + + public SpatialDataset getDataset() { + return this; + } + + public Iterable getAllGeometryNodes(Transaction tx) { + return indexReader.getAllIndexedNodes(tx); + } + + @Override + public boolean containsGeometryNode(Transaction tx, Node geomNode) { + return indexReader.isNodeIndexed(tx, geomNode.getElementId()); + } + + /** + * Provides a method for iterating over all geometries in this dataset. This is similar to the + * getAllGeometryNodes() method but internally converts the Node to a Geometry. + * + * @return iterable over geometries in the dataset + */ + public Iterable getAllGeometries(Transaction tx) { + return new NodeToGeometryIterable(getAllGeometryNodes(tx)); + } + + /** + * In order to wrap one iterable or iterator in another that converts the objects from one type + * to another without loading all into memory, we need to use this ugly java-magic. Man, I miss + * Ruby right now! + */ + private class NodeToGeometryIterable implements Iterable { + + private final Iterator allGeometryNodeIterator; + + private class GeometryIterator implements Iterator { + + public boolean hasNext() { + return NodeToGeometryIterable.this.allGeometryNodeIterator.hasNext(); + } + + public Geometry next() { + return geometryEncoder.decodeGeometry(NodeToGeometryIterable.this.allGeometryNodeIterator.next()); + } + + public void remove() { + } + + } + + NodeToGeometryIterable(Iterable allGeometryNodes) { + this.allGeometryNodeIterator = allGeometryNodes.iterator(); + } + + public Iterator iterator() { + return new GeometryIterator(); + } + + } + + /** + * Return the geometry encoder used by this SpatialDataset to convert individual geometries to + * and from the database structure. + * + * @return GeometryEncoder for this dataset + */ + public GeometryEncoder getGeometryEncoder() { + return geometryEncoder; + } + + /** + * This dataset contains only one layer, itself. + * + * @return iterable over all Layers that can be viewed from this dataset + */ + public Iterable getLayers() { + return Collections.singletonList(this); + } + + /** + * Override this method to provide a style if your layer wishes to control + * its own rendering in the GIS. If a Style is returned, it is used. If a + * File is returned, it is opened and assumed to contain SLD contents. If a + * String is returned, it is assumed to contain SLD contents. + * + * @return null + */ + public Object getStyle() { + return null; + } + + private PropertyMappingManager propertyMappingManager; + + @Override + public PropertyMappingManager getPropertyMappingManager() { + if (propertyMappingManager == null) { + propertyMappingManager = new PropertyMappingManager(this); + } + return propertyMappingManager; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/DynamicLayer.java b/src/main/java/org/neo4j/gis/spatial/DynamicLayer.java index d50c63f1e..13417582c 100644 --- a/src/main/java/org/neo4j/gis/spatial/DynamicLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/DynamicLayer.java @@ -51,175 +51,178 @@ */ public class DynamicLayer extends EditableLayerImpl { - private LinkedHashMap layers; - - private synchronized Map getLayerMap(Transaction tx) { - if (layers == null) { - layers = new LinkedHashMap<>(); - layers.put(getName(), this); - try (var relationships = getLayerNode(tx).getRelationships(Direction.OUTGOING, SpatialRelationshipTypes.LAYER_CONFIG)) { - for (Relationship rel : relationships) { - DynamicLayerConfig config = new DynamicLayerConfig(this, rel.getEndNode()); - layers.put(config.getName(), config); - } - } - } - return layers; - } - - protected boolean removeLayerConfig(Transaction tx, String name) { - Layer layer = getLayerMap(tx).get(name); - if (layer instanceof DynamicLayerConfig) { - synchronized (this) { - DynamicLayerConfig config = (DynamicLayerConfig) layer; - layers = null; // force recalculation of layers cache - Node configNode = config.configNode(tx); - configNode.getSingleRelationship(SpatialRelationshipTypes.LAYER_CONFIG, Direction.INCOMING).delete(); - configNode.delete(); - return true; - } - } else if (layer == null) { - System.out.println("Dynamic layer not found: " + name); - return false; - } else { - System.out.println("Layer is not dynamic and cannot be deleted: " + name); - return false; - } - } - - private static String makeGeometryName(int gtype) { - return SpatialDatabaseService.convertGeometryTypeToName(gtype); - } - - private static String makeGeometryCQL(int gtype) { - return "geometryType(the_geom) = '" + makeGeometryName(gtype) + "'"; - } - - public DynamicLayerConfig addCQLDynamicLayerOnGeometryType(Transaction tx, int gtype) { - return addLayerConfig(tx, "CQL:" + makeGeometryName(gtype), gtype, makeGeometryCQL(gtype)); - } - - public DynamicLayerConfig addCQLDynamicLayerOnAttribute(Transaction tx, String key, String value, int gtype) { - if (value == null) { - return addLayerConfig(tx, "CQL:" + key, gtype, key + " IS NOT NULL AND " + makeGeometryCQL(gtype)); - } else { - // TODO: Better escaping here - //return addLayerConfig("CQL:" + key + "-" + value, gtype, key + " = '" + value + "' AND " + makeGeometryCQL(gtype)); - return addCQLDynamicLayerOnAttributes(tx, new String[]{key, value}, gtype); - } - } - - public DynamicLayerConfig addCQLDynamicLayerOnAttributes(Transaction tx, String[] attributes, int gtype) { - if (attributes == null) { - return addCQLDynamicLayerOnGeometryType(tx, gtype); - } else { - StringBuilder name = new StringBuilder(); - StringBuilder query = new StringBuilder(); - if (gtype != GTYPE_GEOMETRY) { - query.append(makeGeometryCQL(gtype)); - } - for (int i = 0; i < attributes.length; i += 2) { - String key = attributes[i]; - if (name.length() > 0) { - name.append("-"); - } - if (query.length() > 0) { - query.append(" AND "); - } - if (attributes.length > i + 1) { - String value = attributes[i + 1]; - name.append(key).append("-").append(value); - query.append(key).append(" = '").append(value).append("'"); - } else { - name.append(key); - query.append(key).append(" IS NOT NULL"); - } - } - return addLayerConfig(tx, "CQL:" + name.toString(), gtype, query.toString()); - } - } - - public DynamicLayerConfig addLayerConfig(Transaction tx, String name, int type, String query) { - if (!query.startsWith("{")) { - // Not a JSON query, must be CQL, so check the syntax - try { - ECQL.toFilter(query); - } catch (CQLException e) { - throw new SpatialDatabaseException("DynamicLayer query is not JSON and not valid CQL: " + query, e); - } - } - - Layer layer = getLayerMap(tx).get(name); - if (layer != null) { - if (layer instanceof DynamicLayerConfig) { - DynamicLayerConfig config = (DynamicLayerConfig) layer; - if (config.getGeometryType(tx) != type || !config.getQuery().equals(query)) { - System.err.println("Existing LayerConfig with different geometry type or query: " + config); - return null; - } else { - return config; - } - } else { - System.err.println("Existing Layer has same name as requested LayerConfig: " + layer.getName()); - return null; - } - } else synchronized (this) { - DynamicLayerConfig config = new DynamicLayerConfig(tx, this, name, type, query); - layers = null; // force recalculation of layers cache - return config; - } - } - - /** - * Restrict specified layers attributes to the specified set. This will simply - * save the quest to the LayerConfig node, so that future queries will only return - * attributes that are within the named list. If you want to have it perform - * and automatic search, pass null for the names list, but be warned, this can - * take a long time on large datasets. - * - * @param name of layer to restrict - * @param names to use for attributes - */ - public DynamicLayerConfig restrictLayerProperties(Transaction tx, String name, String[] names) { - Layer layer = getLayerMap(tx).get(name); - if (layer != null) { - if (layer instanceof DynamicLayerConfig) { - DynamicLayerConfig config = (DynamicLayerConfig) layer; - if (names == null) { - config.restrictLayerProperties(tx); - } else { - config.setExtraPropertyNames(tx, names); - } - return config; - } else { - System.err.println("Existing Layer has same name as requested LayerConfig: " + layer.getName()); - return null; - } - } else { - System.err.println("No such layer: " + name); - return null; - } - } - - /** - * Restrict specified layers attributes to only those that are actually - * found to be used. This does an exhaustive search and can be time - * consuming. For large layers, consider manually setting the properties - * instead. - */ - public DynamicLayerConfig restrictLayerProperties(Transaction tx, String name) { - return restrictLayerProperties(tx, name, null); - } - - public List getLayerNames(Transaction tx) { - return new ArrayList<>(getLayerMap(tx).keySet()); - } - - public List getLayers(Transaction tx) { - return new ArrayList<>(getLayerMap(tx).values()); - } - - public Layer getLayer(Transaction tx, String name) { - return getLayerMap(tx).get(name); - } + private LinkedHashMap layers; + + private synchronized Map getLayerMap(Transaction tx) { + if (layers == null) { + layers = new LinkedHashMap<>(); + layers.put(getName(), this); + try (var relationships = getLayerNode(tx).getRelationships(Direction.OUTGOING, + SpatialRelationshipTypes.LAYER_CONFIG)) { + for (Relationship rel : relationships) { + DynamicLayerConfig config = new DynamicLayerConfig(this, rel.getEndNode()); + layers.put(config.getName(), config); + } + } + } + return layers; + } + + protected boolean removeLayerConfig(Transaction tx, String name) { + Layer layer = getLayerMap(tx).get(name); + if (layer instanceof DynamicLayerConfig) { + synchronized (this) { + DynamicLayerConfig config = (DynamicLayerConfig) layer; + layers = null; // force recalculation of layers cache + Node configNode = config.configNode(tx); + configNode.getSingleRelationship(SpatialRelationshipTypes.LAYER_CONFIG, Direction.INCOMING).delete(); + configNode.delete(); + return true; + } + } else if (layer == null) { + System.out.println("Dynamic layer not found: " + name); + return false; + } else { + System.out.println("Layer is not dynamic and cannot be deleted: " + name); + return false; + } + } + + private static String makeGeometryName(int gtype) { + return SpatialDatabaseService.convertGeometryTypeToName(gtype); + } + + private static String makeGeometryCQL(int gtype) { + return "geometryType(the_geom) = '" + makeGeometryName(gtype) + "'"; + } + + public DynamicLayerConfig addCQLDynamicLayerOnGeometryType(Transaction tx, int gtype) { + return addLayerConfig(tx, "CQL:" + makeGeometryName(gtype), gtype, makeGeometryCQL(gtype)); + } + + public DynamicLayerConfig addCQLDynamicLayerOnAttribute(Transaction tx, String key, String value, int gtype) { + if (value == null) { + return addLayerConfig(tx, "CQL:" + key, gtype, key + " IS NOT NULL AND " + makeGeometryCQL(gtype)); + } else { + // TODO: Better escaping here + //return addLayerConfig("CQL:" + key + "-" + value, gtype, key + " = '" + value + "' AND " + makeGeometryCQL(gtype)); + return addCQLDynamicLayerOnAttributes(tx, new String[]{key, value}, gtype); + } + } + + public DynamicLayerConfig addCQLDynamicLayerOnAttributes(Transaction tx, String[] attributes, int gtype) { + if (attributes == null) { + return addCQLDynamicLayerOnGeometryType(tx, gtype); + } else { + StringBuilder name = new StringBuilder(); + StringBuilder query = new StringBuilder(); + if (gtype != GTYPE_GEOMETRY) { + query.append(makeGeometryCQL(gtype)); + } + for (int i = 0; i < attributes.length; i += 2) { + String key = attributes[i]; + if (name.length() > 0) { + name.append("-"); + } + if (query.length() > 0) { + query.append(" AND "); + } + if (attributes.length > i + 1) { + String value = attributes[i + 1]; + name.append(key).append("-").append(value); + query.append(key).append(" = '").append(value).append("'"); + } else { + name.append(key); + query.append(key).append(" IS NOT NULL"); + } + } + return addLayerConfig(tx, "CQL:" + name.toString(), gtype, query.toString()); + } + } + + public DynamicLayerConfig addLayerConfig(Transaction tx, String name, int type, String query) { + if (!query.startsWith("{")) { + // Not a JSON query, must be CQL, so check the syntax + try { + ECQL.toFilter(query); + } catch (CQLException e) { + throw new SpatialDatabaseException("DynamicLayer query is not JSON and not valid CQL: " + query, e); + } + } + + Layer layer = getLayerMap(tx).get(name); + if (layer != null) { + if (layer instanceof DynamicLayerConfig) { + DynamicLayerConfig config = (DynamicLayerConfig) layer; + if (config.getGeometryType(tx) != type || !config.getQuery().equals(query)) { + System.err.println("Existing LayerConfig with different geometry type or query: " + config); + return null; + } else { + return config; + } + } else { + System.err.println("Existing Layer has same name as requested LayerConfig: " + layer.getName()); + return null; + } + } else { + synchronized (this) { + DynamicLayerConfig config = new DynamicLayerConfig(tx, this, name, type, query); + layers = null; // force recalculation of layers cache + return config; + } + } + } + + /** + * Restrict specified layers attributes to the specified set. This will simply + * save the quest to the LayerConfig node, so that future queries will only return + * attributes that are within the named list. If you want to have it perform + * and automatic search, pass null for the names list, but be warned, this can + * take a long time on large datasets. + * + * @param name of layer to restrict + * @param names to use for attributes + */ + public DynamicLayerConfig restrictLayerProperties(Transaction tx, String name, String[] names) { + Layer layer = getLayerMap(tx).get(name); + if (layer != null) { + if (layer instanceof DynamicLayerConfig) { + DynamicLayerConfig config = (DynamicLayerConfig) layer; + if (names == null) { + config.restrictLayerProperties(tx); + } else { + config.setExtraPropertyNames(tx, names); + } + return config; + } else { + System.err.println("Existing Layer has same name as requested LayerConfig: " + layer.getName()); + return null; + } + } else { + System.err.println("No such layer: " + name); + return null; + } + } + + /** + * Restrict specified layers attributes to only those that are actually + * found to be used. This does an exhaustive search and can be time + * consuming. For large layers, consider manually setting the properties + * instead. + */ + public DynamicLayerConfig restrictLayerProperties(Transaction tx, String name) { + return restrictLayerProperties(tx, name, null); + } + + public List getLayerNames(Transaction tx) { + return new ArrayList<>(getLayerMap(tx).keySet()); + } + + public List getLayers(Transaction tx) { + return new ArrayList<>(getLayerMap(tx).values()); + } + + public Layer getLayer(Transaction tx, String name) { + return getLayerMap(tx).get(name); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/DynamicLayerConfig.java b/src/main/java/org/neo4j/gis/spatial/DynamicLayerConfig.java index c480e45ea..548d920fd 100644 --- a/src/main/java/org/neo4j/gis/spatial/DynamicLayerConfig.java +++ b/src/main/java/org/neo4j/gis/spatial/DynamicLayerConfig.java @@ -24,6 +24,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.filter.text.cql2.CQLException; import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.attributes.PropertyMappingManager; @@ -37,262 +38,270 @@ import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; public class DynamicLayerConfig implements Layer, Constants { - private final DynamicLayer parent; - private final String name; - private final int geometryType; - private final String query; - protected String configNodeId; - private String[] propertyNames; - - /** - * Construct the layer config instance on existing config information in the database. - */ - public DynamicLayerConfig(DynamicLayer parent, Node configNode) { - this.parent = parent; - this.name = (String) configNode.getProperty(PROP_LAYER); - this.geometryType = (Integer) configNode.getProperty(PROP_TYPE); - this.query = (String) configNode.getProperty(PROP_QUERY); - this.configNodeId = configNode.getElementId(); - this.propertyNames = (String[]) configNode.getProperty("propertyNames", null); - } - - /** - * Construct a new layer config by building the database structure to support the necessary configuration - * - * @param tx the Transaction in which this config is created - * @param parent the DynamicLayer containing this config - * @param name of the new dynamic layer - * @param geometryType the geometry this layer supports - * @param query formatted query string for this dynamic layer - */ - public DynamicLayerConfig(Transaction tx, DynamicLayer parent, String name, int geometryType, String query) { - this.parent = parent; - Node node = tx.createNode(); - node.setProperty(PROP_LAYER, name); - node.setProperty(PROP_TYPE, geometryType); - node.setProperty(PROP_QUERY, query); - parent.getLayerNode(tx).createRelationshipTo(node, SpatialRelationshipTypes.LAYER_CONFIG); - this.name = name; - this.geometryType = geometryType; - this.query = query; - configNodeId = node.getElementId(); - } - - @Override - public String getName() { - return name; - } - - public String getQuery() { - return query; - } - - @Override - public SpatialDatabaseRecord add(Transaction tx, Node geomNode) { - throw new SpatialDatabaseException("Cannot add nodes to dynamic layers, add the node to the base layer instead"); - } - - @Override - public int addAll(Transaction tx, List geomNodes) { - throw new SpatialDatabaseException("Cannot add nodes to dynamic layers, add the node to the base layer instead"); - } - - @Override - public void delete(Transaction tx, Listener monitor) { - throw new SpatialDatabaseException("Cannot delete dynamic layers, delete the base layer instead"); - } - - @Override - public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { - return parent.getCoordinateReferenceSystem(tx); - } - - @Override - public SpatialDataset getDataset() { - return parent.getDataset(); - } - - @Override - public String[] getExtraPropertyNames(Transaction tx) { - if (propertyNames != null && propertyNames.length > 0) { - return propertyNames; - } else { - return parent.getExtraPropertyNames(tx); - } - } - - private static class PropertyUsageSearch implements SearchFilter { - - private final Layer layer; - private final LinkedHashMap names = new LinkedHashMap<>(); - private int nodeCount = 0; - private final int MAX_COUNT = 10000; - - public PropertyUsageSearch(Layer layer) { - this.layer = layer; - } - - @Override - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return nodeCount < MAX_COUNT; - } - - @Override - public boolean geometryMatches(Transaction tx, Node geomNode) { - if (nodeCount++ < MAX_COUNT) { - SpatialDatabaseRecord record = new SpatialDatabaseRecord(layer, geomNode); - for (String name : record.getPropertyNames(tx)) { - Object value = record.getProperty(tx, name); - if (value != null) { - Integer count = names.get(name); - if (count == null) - count = 0; - names.put(name, count + 1); - } - } - } - - // no need to collect nodes - return false; - } - - public String[] getNames() { - return names.keySet().toArray(new String[]{}); - } - - public int getNodeCount() { - return nodeCount; - } - - public void describeUsage(PrintStream out) { - for (String name : names.keySet()) { - out.println(name + "\t" + names.get(name)); - } - } - } - - /** - * This method will scan the layer for property names that are actually - * used, and restrict the layer properties to those - */ - public void restrictLayerProperties(Transaction tx) { - if (propertyNames != null && propertyNames.length > 0) { - System.out.println("Restricted property names already exists - will be overwritten"); - } - System.out.println("Before property scan we have " + getExtraPropertyNames(tx).length + " known attributes for layer " + getName()); - - PropertyUsageSearch search = new PropertyUsageSearch(this); - getIndex().searchIndex(tx, search).count(); - setExtraPropertyNames(tx, search.getNames()); - - System.out.println("After property scan of " + search.getNodeCount() + " nodes, we have " + getExtraPropertyNames(tx).length + " known attributes for layer " + getName()); - // search.describeUsage(System.out); - } - - public Node configNode(Transaction tx) { - return tx.getNodeByElementId(configNodeId); - } - - public void setExtraPropertyNames(Transaction tx, String[] names) { - configNode(tx).setProperty("propertyNames", names); - propertyNames = names; - } - - @Override - public GeometryEncoder getGeometryEncoder() { - return parent.getGeometryEncoder(); - } - - @Override - public GeometryFactory getGeometryFactory() { - return parent.getGeometryFactory(); - } - - @Override - public Integer getGeometryType(Transaction tx) { - return (Integer) configNode(tx).getProperty(PROP_TYPE); - } - - @Override - public LayerIndexReader getIndex() { - if (parent.indexReader instanceof LayerTreeIndexReader) { - String query = getQuery(); - if (query.startsWith("{")) { - // Make a standard JSON based dynamic layer - return new DynamicIndexReader((LayerTreeIndexReader) parent.indexReader, query); - } else { - // Make a CQL based dynamic layer - try { - return new CQLIndexReader((LayerTreeIndexReader) parent.indexReader, this, query); - } catch (CQLException e) { - throw new SpatialDatabaseException("Error while creating CQL based DynamicLayer", e); - } - } - } else { - throw new SpatialDatabaseException("Cannot make a DynamicLayer from a non-LayerTreeIndexReader Layer"); - } - } - - @Override - public Node getLayerNode(Transaction tx) { - // TODO: Make sure that the mismatch between the name on the dynamic - // layer node and the dynamic layer translates into the correct - // object being returned - return parent.getLayerNode(tx); - } - - @Override - public void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode) { - throw new SpatialDatabaseException("Cannot initialize the layer config, initialize only the dynamic layer node"); - } - - @Override - public Object getStyle() { - Object style = parent.getStyle(); - if (style instanceof File) { - File parent = ((File) style).getParentFile(); - File newStyle = new File(parent, getName() + ".sld"); - if (newStyle.canRead()) { - style = newStyle; - } - } - return style; - } - - public Layer getParent() { - return parent; - } - - @Override - public String toString() { - return getName(); - } - - private PropertyMappingManager propertyMappingManager; - - @Override - public PropertyMappingManager getPropertyMappingManager() { - if (propertyMappingManager == null) { - propertyMappingManager = new PropertyMappingManager(this); - } - return propertyMappingManager; - } - - protected Map getConfig() { - Map config = new LinkedHashMap<>(); - config.put("layer", this.name); - config.put("type", String.valueOf(this.geometryType)); - config.put("query", this.query); - return config; - } - - @Override - public String getSignature() { - Map config = getConfig(); - return "DynamicLayer(name='" + getName() + "', config={layer='" + config.get("layer") + "', query=\"" + config.get("query") + "\"})"; - } + private final DynamicLayer parent; + private final String name; + private final int geometryType; + private final String query; + protected String configNodeId; + private String[] propertyNames; + + /** + * Construct the layer config instance on existing config information in the database. + */ + public DynamicLayerConfig(DynamicLayer parent, Node configNode) { + this.parent = parent; + this.name = (String) configNode.getProperty(PROP_LAYER); + this.geometryType = (Integer) configNode.getProperty(PROP_TYPE); + this.query = (String) configNode.getProperty(PROP_QUERY); + this.configNodeId = configNode.getElementId(); + this.propertyNames = (String[]) configNode.getProperty("propertyNames", null); + } + + /** + * Construct a new layer config by building the database structure to support the necessary configuration + * + * @param tx the Transaction in which this config is created + * @param parent the DynamicLayer containing this config + * @param name of the new dynamic layer + * @param geometryType the geometry this layer supports + * @param query formatted query string for this dynamic layer + */ + public DynamicLayerConfig(Transaction tx, DynamicLayer parent, String name, int geometryType, String query) { + this.parent = parent; + Node node = tx.createNode(); + node.setProperty(PROP_LAYER, name); + node.setProperty(PROP_TYPE, geometryType); + node.setProperty(PROP_QUERY, query); + parent.getLayerNode(tx).createRelationshipTo(node, SpatialRelationshipTypes.LAYER_CONFIG); + this.name = name; + this.geometryType = geometryType; + this.query = query; + configNodeId = node.getElementId(); + } + + @Override + public String getName() { + return name; + } + + public String getQuery() { + return query; + } + + @Override + public SpatialDatabaseRecord add(Transaction tx, Node geomNode) { + throw new SpatialDatabaseException( + "Cannot add nodes to dynamic layers, add the node to the base layer instead"); + } + + @Override + public int addAll(Transaction tx, List geomNodes) { + throw new SpatialDatabaseException( + "Cannot add nodes to dynamic layers, add the node to the base layer instead"); + } + + @Override + public void delete(Transaction tx, Listener monitor) { + throw new SpatialDatabaseException("Cannot delete dynamic layers, delete the base layer instead"); + } + + @Override + public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { + return parent.getCoordinateReferenceSystem(tx); + } + + @Override + public SpatialDataset getDataset() { + return parent.getDataset(); + } + + @Override + public String[] getExtraPropertyNames(Transaction tx) { + if (propertyNames != null && propertyNames.length > 0) { + return propertyNames; + } else { + return parent.getExtraPropertyNames(tx); + } + } + + private static class PropertyUsageSearch implements SearchFilter { + + private final Layer layer; + private final LinkedHashMap names = new LinkedHashMap<>(); + private int nodeCount = 0; + private final int MAX_COUNT = 10000; + + public PropertyUsageSearch(Layer layer) { + this.layer = layer; + } + + @Override + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return nodeCount < MAX_COUNT; + } + + @Override + public boolean geometryMatches(Transaction tx, Node geomNode) { + if (nodeCount++ < MAX_COUNT) { + SpatialDatabaseRecord record = new SpatialDatabaseRecord(layer, geomNode); + for (String name : record.getPropertyNames(tx)) { + Object value = record.getProperty(tx, name); + if (value != null) { + Integer count = names.get(name); + if (count == null) { + count = 0; + } + names.put(name, count + 1); + } + } + } + + // no need to collect nodes + return false; + } + + public String[] getNames() { + return names.keySet().toArray(new String[]{}); + } + + public int getNodeCount() { + return nodeCount; + } + + public void describeUsage(PrintStream out) { + for (String name : names.keySet()) { + out.println(name + "\t" + names.get(name)); + } + } + } + + /** + * This method will scan the layer for property names that are actually + * used, and restrict the layer properties to those + */ + public void restrictLayerProperties(Transaction tx) { + if (propertyNames != null && propertyNames.length > 0) { + System.out.println("Restricted property names already exists - will be overwritten"); + } + System.out.println( + "Before property scan we have " + getExtraPropertyNames(tx).length + " known attributes for layer " + + getName()); + + PropertyUsageSearch search = new PropertyUsageSearch(this); + getIndex().searchIndex(tx, search).count(); + setExtraPropertyNames(tx, search.getNames()); + + System.out.println( + "After property scan of " + search.getNodeCount() + " nodes, we have " + getExtraPropertyNames( + tx).length + " known attributes for layer " + getName()); + // search.describeUsage(System.out); + } + + public Node configNode(Transaction tx) { + return tx.getNodeByElementId(configNodeId); + } + + public void setExtraPropertyNames(Transaction tx, String[] names) { + configNode(tx).setProperty("propertyNames", names); + propertyNames = names; + } + + @Override + public GeometryEncoder getGeometryEncoder() { + return parent.getGeometryEncoder(); + } + + @Override + public GeometryFactory getGeometryFactory() { + return parent.getGeometryFactory(); + } + + @Override + public Integer getGeometryType(Transaction tx) { + return (Integer) configNode(tx).getProperty(PROP_TYPE); + } + + @Override + public LayerIndexReader getIndex() { + if (parent.indexReader instanceof LayerTreeIndexReader) { + String query = getQuery(); + if (query.startsWith("{")) { + // Make a standard JSON based dynamic layer + return new DynamicIndexReader((LayerTreeIndexReader) parent.indexReader, query); + } else { + // Make a CQL based dynamic layer + try { + return new CQLIndexReader((LayerTreeIndexReader) parent.indexReader, this, query); + } catch (CQLException e) { + throw new SpatialDatabaseException("Error while creating CQL based DynamicLayer", e); + } + } + } else { + throw new SpatialDatabaseException("Cannot make a DynamicLayer from a non-LayerTreeIndexReader Layer"); + } + } + + @Override + public Node getLayerNode(Transaction tx) { + // TODO: Make sure that the mismatch between the name on the dynamic + // layer node and the dynamic layer translates into the correct + // object being returned + return parent.getLayerNode(tx); + } + + @Override + public void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode) { + throw new SpatialDatabaseException( + "Cannot initialize the layer config, initialize only the dynamic layer node"); + } + + @Override + public Object getStyle() { + Object style = parent.getStyle(); + if (style instanceof File) { + File parent = ((File) style).getParentFile(); + File newStyle = new File(parent, getName() + ".sld"); + if (newStyle.canRead()) { + style = newStyle; + } + } + return style; + } + + public Layer getParent() { + return parent; + } + + @Override + public String toString() { + return getName(); + } + + private PropertyMappingManager propertyMappingManager; + + @Override + public PropertyMappingManager getPropertyMappingManager() { + if (propertyMappingManager == null) { + propertyMappingManager = new PropertyMappingManager(this); + } + return propertyMappingManager; + } + + protected Map getConfig() { + Map config = new LinkedHashMap<>(); + config.put("layer", this.name); + config.put("type", String.valueOf(this.geometryType)); + config.put("query", this.query); + return config; + } + + @Override + public String getSignature() { + Map config = getConfig(); + return "DynamicLayer(name='" + getName() + "', config={layer='" + config.get("layer") + "', query=\"" + + config.get("query") + "\"})"; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/EditableLayer.java b/src/main/java/org/neo4j/gis/spatial/EditableLayer.java index 04feac35f..1bf73da60 100644 --- a/src/main/java/org/neo4j/gis/spatial/EditableLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/EditableLayer.java @@ -19,10 +19,9 @@ */ package org.neo4j.gis.spatial; -import org.neo4j.graphdb.Transaction; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - import org.locationtech.jts.geom.Geometry; +import org.neo4j.graphdb.Transaction; /** * Instances of Layer provide the ability for developers to add/remove and edit geometries @@ -34,30 +33,31 @@ */ public interface EditableLayer extends Layer { - /** - * Add a new geometry to the layer. This will add the geometry to the index. - */ - SpatialDatabaseRecord add(Transaction tx, Geometry geometry); - - /** - * Add a new geometry to the layer. This will add the geometry to the index. - * @TODO: Rather use a HashMap of properties - */ - SpatialDatabaseRecord add(Transaction tx, Geometry geometry, String[] fieldsName, Object[] fields); - - /** - * Delete the geometry identified by the passed node id. This might be as simple as deleting the - * geometry node, or it might require extracting and deleting an entire sub-graph. - */ - void delete(Transaction tx, String geometryNodeId); - - /** - * Update the geometry identified by the passed node id. This might be as simple as changing - * node properties or it might require editing an entire sub-graph. - */ - void update(Transaction tx, String geometryNodeId, Geometry geometry); + /** + * Add a new geometry to the layer. This will add the geometry to the index. + */ + SpatialDatabaseRecord add(Transaction tx, Geometry geometry); + + /** + * Add a new geometry to the layer. This will add the geometry to the index. + * + * @TODO: Rather use a HashMap of properties + */ + SpatialDatabaseRecord add(Transaction tx, Geometry geometry, String[] fieldsName, Object[] fields); + + /** + * Delete the geometry identified by the passed node id. This might be as simple as deleting the + * geometry node, or it might require extracting and deleting an entire sub-graph. + */ + void delete(Transaction tx, String geometryNodeId); + + /** + * Update the geometry identified by the passed node id. This might be as simple as changing + * node properties or it might require editing an entire sub-graph. + */ + void update(Transaction tx, String geometryNodeId, Geometry geometry); void setCoordinateReferenceSystem(Transaction tx, CoordinateReferenceSystem coordinateReferenceSystem); - void removeFromIndex(Transaction tx, String geomNodeId); + void removeFromIndex(Transaction tx, String geomNodeId); } diff --git a/src/main/java/org/neo4j/gis/spatial/EditableLayerImpl.java b/src/main/java/org/neo4j/gis/spatial/EditableLayerImpl.java index 614e1aa3d..57b1e369d 100644 --- a/src/main/java/org/neo4j/gis/spatial/EditableLayerImpl.java +++ b/src/main/java/org/neo4j/gis/spatial/EditableLayerImpl.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial; +import org.locationtech.jts.geom.Geometry; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.locationtech.jts.geom.Geometry; - public class EditableLayerImpl extends DefaultLayer implements EditableLayer { /** @@ -44,20 +43,20 @@ public SpatialDatabaseRecord add(Transaction tx, Geometry geometry, String[] fie } @Override - public void update(Transaction tx, String geomNodeId, Geometry geometry) { + public void update(Transaction tx, String geomNodeId, Geometry geometry) { indexWriter.remove(tx, geomNodeId, false, true); - Node geomNode = tx.getNodeByElementId(geomNodeId); + Node geomNode = tx.getNodeByElementId(geomNodeId); getGeometryEncoder().encodeGeometry(tx, geometry, geomNode); indexWriter.add(tx, geomNode); } @Override - public void delete(Transaction tx, String geomNodeId) { + public void delete(Transaction tx, String geomNodeId) { indexWriter.remove(tx, geomNodeId, true, false); } @Override - public void removeFromIndex(Transaction tx, String geomNodeId) { + public void removeFromIndex(Transaction tx, String geomNodeId) { final boolean deleteGeomNode = false; indexWriter.remove(tx, geomNodeId, deleteGeomNode, false); } diff --git a/src/main/java/org/neo4j/gis/spatial/GeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/GeometryEncoder.java index bb0f2a30b..bce98d700 100644 --- a/src/main/java/org/neo4j/gis/spatial/GeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/GeometryEncoder.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.rtree.EnvelopeDecoder; -import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Entity; - -import org.locationtech.jts.geom.Geometry; +import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; @@ -49,15 +48,15 @@ */ public interface GeometryEncoder extends EnvelopeDecoder { - /** - * When accessing an existing layer, the Layer is constructed from a single node in the graph - * that represents a layer. This node is expected to have a property containing the class name - * of the GeometryEncoder for that layer, and it will be constructed and passed the layer using - * this method, allowing the Layer and the GeometryEncoder to interact. - * - * @param layer recently created Layer class - */ - void init(Layer layer); + /** + * When accessing an existing layer, the Layer is constructed from a single node in the graph + * that represents a layer. This node is expected to have a property containing the class name + * of the GeometryEncoder for that layer, and it will be constructed and passed the layer using + * this method, allowing the Layer and the GeometryEncoder to interact. + * + * @param layer recently created Layer class + */ + void init(Layer layer); /** * This method is called to store a bounding box for the geometry to the database. It should write it to the @@ -72,10 +71,10 @@ public interface GeometryEncoder extends EnvelopeDecoder { void encodeGeometry(Transaction tx, Geometry geometry, Entity container); /** - * This method is called on an individual container when we need to extract the geometry. If the - * container is a node, this could be the root of a sub-graph containing the geometry. - */ - Geometry decodeGeometry(Entity container); + * This method is called on an individual container when we need to extract the geometry. If the + * container is a node, this could be the root of a sub-graph containing the geometry. + */ + Geometry decodeGeometry(Entity container); /** * Each geometry might have a set of associated attributes, or properties. @@ -97,7 +96,8 @@ public interface GeometryEncoder extends EnvelopeDecoder { /** * For external expression of the configuration of this geometry encoder + * * @return descriptive signature of encoder, type and configuration - */ + */ String getSignature(); -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/Layer.java b/src/main/java/org/neo4j/gis/spatial/Layer.java index 91606f040..a1ef69b1b 100644 --- a/src/main/java/org/neo4j/gis/spatial/Layer.java +++ b/src/main/java/org/neo4j/gis/spatial/Layer.java @@ -19,17 +19,15 @@ */ package org.neo4j.gis.spatial; +import java.util.List; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.locationtech.jts.geom.GeometryFactory; +import org.neo4j.gis.spatial.attributes.PropertyMappingManager; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.index.LayerIndexReader; import org.neo4j.gis.spatial.rtree.Listener; -import org.neo4j.gis.spatial.attributes.PropertyMappingManager; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import org.locationtech.jts.geom.GeometryFactory; - -import java.util.List; /** @@ -42,100 +40,102 @@ */ public interface Layer { - /** - * The layer is constructed from metadata in the layer node, which requires that the layer have - * a no-argument constructor. The real initialization of the layer is then performed by calling - * this method. The layer implementation can store the passed parameters for later use - * satisfying the prupose of the layer API (see other Layer methods). - */ - void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode); - - /** - * Every layer using a specific implementation of the SpatialIndexReader and SpatialIndexWriter - * for indexing the data in that layer. - * - * @return the SpatialIndexReader used to perform searches on the data in the layer - */ - LayerIndexReader getIndex(); - - /** - * This method adds existing geometries to the layer for indexing. After this method is called the geometry should be searchable. - * - * @param geomNode - * @return SpatialDatabaseRecord representation of the geometry added to the database - */ - SpatialDatabaseRecord add(Transaction tx, Node geomNode); - - /** - * This method adds existing geometries to the layer for indexing in bulk. After this method is called the geometry should be searchable. - * - * @param geomNodes - * @return the number of geometries added to the database - */ - int addAll(Transaction tx, List geomNodes); - - GeometryFactory getGeometryFactory(); - - /** - * All layers are associated with a single node in the database. This node will have properties, - * relationships (sub-graph) or both to describe the contents of the layer - */ - Node getLayerNode(Transaction tx); - - /** - * Delete the entire layer, including the index. The specific layer implementation will decide - * if this method should delete also the geometry nodes indexed by this layer. Some - * implementations have data that only has meaning within a layer, and so will be deleted. - * Others are simply views onto other more complex data models and deleting the geometry nodes - * might imply damage to the model. Keep this in mind when coding implementations of the Layer. - */ - void delete(Transaction tx, Listener monitor); - - /** - * Every layer is defined by a unique name. Uniqueness is not enforced, but lack of uniqueness - * will not guarrantee the right layer returned from a search. - */ - String getName(); - - /** - * Each layer can contain geometries stored in the database in a custom way. Classes that - * implement the layer should also provide appropriate GeometryEncoders for encoding and - * decoding the geometries. This can be either as properties of a geometry node, or as - * sub-graphs accessible from some geometry node. - * - * @return implementation of the GemoetryEncoder class enabling encoding/decoding of geometries - * from the graph - */ - GeometryEncoder getGeometryEncoder(); - - /** - * Each layer can represent data stored in a specific coordinate refernece system, or - * projection. - */ - CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx); - - /** - * Each layer contains geometries with optional attributes. - * - * @return String array of all attribute names - * @param tx - */ - String[] getExtraPropertyNames(Transaction tx); - - /** - * The layer conforms with the Geotools pattern of only allowing a single geometry per layer. - * - * @return integer key for the geotools geometry type - */ - Integer getGeometryType(Transaction tx); - - /** - * Each layer is associated with a SpatialDataset. This can be a one-for-one match to the layer, - * or can be expressed as many layers on a single dataset. - * - * @return SpatialDataset containing the data indexed by this layer. - */ - SpatialDataset getDataset(); + /** + * The layer is constructed from metadata in the layer node, which requires that the layer have + * a no-argument constructor. The real initialization of the layer is then performed by calling + * this method. The layer implementation can store the passed parameters for later use + * satisfying the prupose of the layer API (see other Layer methods). + */ + void initialize(Transaction tx, IndexManager indexManager, String name, Node layerNode); + + /** + * Every layer using a specific implementation of the SpatialIndexReader and SpatialIndexWriter + * for indexing the data in that layer. + * + * @return the SpatialIndexReader used to perform searches on the data in the layer + */ + LayerIndexReader getIndex(); + + /** + * This method adds existing geometries to the layer for indexing. After this method is called the geometry should + * be searchable. + * + * @param geomNode + * @return SpatialDatabaseRecord representation of the geometry added to the database + */ + SpatialDatabaseRecord add(Transaction tx, Node geomNode); + + /** + * This method adds existing geometries to the layer for indexing in bulk. After this method is called the geometry + * should be searchable. + * + * @param geomNodes + * @return the number of geometries added to the database + */ + int addAll(Transaction tx, List geomNodes); + + GeometryFactory getGeometryFactory(); + + /** + * All layers are associated with a single node in the database. This node will have properties, + * relationships (sub-graph) or both to describe the contents of the layer + */ + Node getLayerNode(Transaction tx); + + /** + * Delete the entire layer, including the index. The specific layer implementation will decide + * if this method should delete also the geometry nodes indexed by this layer. Some + * implementations have data that only has meaning within a layer, and so will be deleted. + * Others are simply views onto other more complex data models and deleting the geometry nodes + * might imply damage to the model. Keep this in mind when coding implementations of the Layer. + */ + void delete(Transaction tx, Listener monitor); + + /** + * Every layer is defined by a unique name. Uniqueness is not enforced, but lack of uniqueness + * will not guarrantee the right layer returned from a search. + */ + String getName(); + + /** + * Each layer can contain geometries stored in the database in a custom way. Classes that + * implement the layer should also provide appropriate GeometryEncoders for encoding and + * decoding the geometries. This can be either as properties of a geometry node, or as + * sub-graphs accessible from some geometry node. + * + * @return implementation of the GemoetryEncoder class enabling encoding/decoding of geometries + * from the graph + */ + GeometryEncoder getGeometryEncoder(); + + /** + * Each layer can represent data stored in a specific coordinate refernece system, or + * projection. + */ + CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx); + + /** + * Each layer contains geometries with optional attributes. + * + * @param tx + * @return String array of all attribute names + */ + String[] getExtraPropertyNames(Transaction tx); + + /** + * The layer conforms with the Geotools pattern of only allowing a single geometry per layer. + * + * @return integer key for the geotools geometry type + */ + Integer getGeometryType(Transaction tx); + + /** + * Each layer is associated with a SpatialDataset. This can be a one-for-one match to the layer, + * or can be expressed as many layers on a single dataset. + * + * @return SpatialDataset containing the data indexed by this layer. + */ + SpatialDataset getDataset(); /** * Each layer can optionally provide a style to be used in rendering this @@ -148,11 +148,12 @@ public interface Layer { */ Object getStyle(); - PropertyMappingManager getPropertyMappingManager(); + PropertyMappingManager getPropertyMappingManager(); - /** - * For external expression of the configuration of this layer - * @return descriptive signature of layer, name, type and encoder - */ - String getSignature(); + /** + * For external expression of the configuration of this layer + * + * @return descriptive signature of layer, name, type and encoder + */ + String getSignature(); } diff --git a/src/main/java/org/neo4j/gis/spatial/LineStringNetworkGenerator.java b/src/main/java/org/neo4j/gis/spatial/LineStringNetworkGenerator.java index bc8f72b11..f8cf1738e 100644 --- a/src/main/java/org/neo4j/gis/spatial/LineStringNetworkGenerator.java +++ b/src/main/java/org/neo4j/gis/spatial/LineStringNetworkGenerator.java @@ -20,13 +20,11 @@ package org.neo4j.gis.spatial; import java.util.Iterator; - -import org.neo4j.gis.spatial.filter.SearchIntersect; -import org.neo4j.graphdb.Node; - import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; +import org.neo4j.gis.spatial.filter.SearchIntersect; +import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; /** @@ -35,69 +33,73 @@ * the two LineStrings are connected together with a Relationship. */ public class LineStringNetworkGenerator { - private final EditableLayer pointsLayer; - private final EditableLayer edgesLayer; - private final Double buffer; - public LineStringNetworkGenerator(EditableLayer pointsLayer, EditableLayer edgesLayer) { - this(pointsLayer, edgesLayer, null); - } + private final EditableLayer pointsLayer; + private final EditableLayer edgesLayer; + private final Double buffer; + + public LineStringNetworkGenerator(EditableLayer pointsLayer, EditableLayer edgesLayer) { + this(pointsLayer, edgesLayer, null); + } - public LineStringNetworkGenerator(EditableLayer pointsLayer, EditableLayer edgesLayer, Double buffer) { - this.pointsLayer = pointsLayer; - this.edgesLayer = edgesLayer; - this.buffer = buffer; - } + public LineStringNetworkGenerator(EditableLayer pointsLayer, EditableLayer edgesLayer, Double buffer) { + this.pointsLayer = pointsLayer; + this.edgesLayer = edgesLayer; + this.buffer = buffer; + } - public void add(Transaction tx, SpatialDatabaseRecord record) { - Geometry geometry = record.getGeometry(); - if (geometry instanceof MultiLineString) { - add(tx, (MultiLineString) geometry, record); - } else if (geometry instanceof LineString) { - add(tx, (LineString) geometry, record); - } else { - // TODO better handling? - throw new IllegalArgumentException("geometry type not supported: " + geometry.getGeometryType()); - } - } + public void add(Transaction tx, SpatialDatabaseRecord record) { + Geometry geometry = record.getGeometry(); + if (geometry instanceof MultiLineString) { + add(tx, (MultiLineString) geometry, record); + } else if (geometry instanceof LineString) { + add(tx, (LineString) geometry, record); + } else { + // TODO better handling? + throw new IllegalArgumentException("geometry type not supported: " + geometry.getGeometryType()); + } + } - public void add(Transaction tx, MultiLineString lines) { - add(tx, lines, null); - } + public void add(Transaction tx, MultiLineString lines) { + add(tx, lines, null); + } - public void add(Transaction tx, LineString line) { - add(tx, line, null); - } + public void add(Transaction tx, LineString line) { + add(tx, line, null); + } - protected void add(Transaction tx, MultiLineString line, SpatialDatabaseRecord record) { - for (int i = 0; i < line.getNumGeometries(); i++) { - add(tx, (LineString) line.getGeometryN(i), record); - } - } + protected void add(Transaction tx, MultiLineString line, SpatialDatabaseRecord record) { + for (int i = 0; i < line.getNumGeometries(); i++) { + add(tx, (LineString) line.getGeometryN(i), record); + } + } - protected void add(Transaction tx, LineString line, SpatialDatabaseRecord edge) { - if (edge == null) { - edge = edgesLayer.add(tx, line); - } + protected void add(Transaction tx, LineString line, SpatialDatabaseRecord edge) { + if (edge == null) { + edge = edgesLayer.add(tx, line); + } - // TODO reserved property? - edge.setProperty("_network_length", edge.getGeometry().getLength()); + // TODO reserved property? + edge.setProperty("_network_length", edge.getGeometry().getLength()); - addEdgePoint(tx, edge.getGeomNode(), line.getStartPoint()); - addEdgePoint(tx, edge.getGeomNode(), line.getEndPoint()); - } + addEdgePoint(tx, edge.getGeomNode(), line.getStartPoint()); + addEdgePoint(tx, edge.getGeomNode(), line.getEndPoint()); + } - protected void addEdgePoint(Transaction tx, Node edge, Geometry edgePoint) { - if (buffer != null) edgePoint = edgePoint.buffer(buffer.doubleValue()); + protected void addEdgePoint(Transaction tx, Node edge, Geometry edgePoint) { + if (buffer != null) { + edgePoint = edgePoint.buffer(buffer.doubleValue()); + } - Iterator results = pointsLayer.getIndex().search(tx, new SearchIntersect(pointsLayer, edgePoint)); - if (!results.hasNext()) { - SpatialDatabaseRecord point = pointsLayer.add(tx, edgePoint); - edge.createRelationshipTo(point.getGeomNode(), SpatialRelationshipTypes.NETWORK); - } else { - while (results.hasNext()) { - edge.createRelationshipTo(results.next().getGeomNode(), SpatialRelationshipTypes.NETWORK); - } - } - } -} \ No newline at end of file + Iterator results = pointsLayer.getIndex() + .search(tx, new SearchIntersect(pointsLayer, edgePoint)); + if (!results.hasNext()) { + SpatialDatabaseRecord point = pointsLayer.add(tx, edgePoint); + edge.createRelationshipTo(point.getGeomNode(), SpatialRelationshipTypes.NETWORK); + } else { + while (results.hasNext()) { + edge.createRelationshipTo(results.next().getGeomNode(), SpatialRelationshipTypes.NETWORK); + } + } + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/OrderedEditableLayer.java b/src/main/java/org/neo4j/gis/spatial/OrderedEditableLayer.java index 0f177e71a..33ac83ac4 100644 --- a/src/main/java/org/neo4j/gis/spatial/OrderedEditableLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/OrderedEditableLayer.java @@ -19,6 +19,8 @@ */ package org.neo4j.gis.spatial; +import static org.neo4j.gis.spatial.utilities.TraverserFactory.createTraverserInBackwardsCompatibleWay; + import org.locationtech.jts.geom.Geometry; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Node; @@ -29,8 +31,6 @@ import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; -import static org.neo4j.gis.spatial.utilities.TraverserFactory.createTraverserInBackwardsCompatibleWay; - /** * This class extends the EditableLayerImpl in a way that allows for the * geometry order to be maintained. If the user wishes to iterate through the @@ -43,50 +43,51 @@ * non-ordered for a simpler data structure. */ public class OrderedEditableLayer extends EditableLayerImpl { - private Node previousGeomNode; - enum OrderedRelationshipTypes implements RelationshipType { - GEOMETRIES, NEXT_GEOM - } + private Node previousGeomNode; + + enum OrderedRelationshipTypes implements RelationshipType { + GEOMETRIES, NEXT_GEOM + } - protected Node addGeomNode(Transaction tx, Geometry geom, String[] fieldsName, Object[] fields) { - Node geomNode = super.addGeomNode(tx, geom, fieldsName, fields); - Node layerNode = getLayerNode(tx); - if (previousGeomNode == null) { - TraversalDescription traversalDescription = new MonoDirectionalTraversalDescription() - .order(BranchOrderingPolicies.POSTORDER_BREADTH_FIRST) - .relationships(OrderedRelationshipTypes.GEOMETRIES, Direction.INCOMING) - .relationships(OrderedRelationshipTypes.NEXT_GEOM, Direction.INCOMING) - .evaluator(Evaluators.excludeStartPosition()); - for (Node node : createTraverserInBackwardsCompatibleWay(traversalDescription, layerNode).nodes()) { - previousGeomNode = node; - } - } - if (previousGeomNode != null) { - previousGeomNode.createRelationshipTo(geomNode, OrderedRelationshipTypes.NEXT_GEOM); - } else { - layerNode.createRelationshipTo(geomNode, OrderedRelationshipTypes.GEOMETRIES); - } - previousGeomNode = geomNode; - return geomNode; - } + protected Node addGeomNode(Transaction tx, Geometry geom, String[] fieldsName, Object[] fields) { + Node geomNode = super.addGeomNode(tx, geom, fieldsName, fields); + Node layerNode = getLayerNode(tx); + if (previousGeomNode == null) { + TraversalDescription traversalDescription = new MonoDirectionalTraversalDescription() + .order(BranchOrderingPolicies.POSTORDER_BREADTH_FIRST) + .relationships(OrderedRelationshipTypes.GEOMETRIES, Direction.INCOMING) + .relationships(OrderedRelationshipTypes.NEXT_GEOM, Direction.INCOMING) + .evaluator(Evaluators.excludeStartPosition()); + for (Node node : createTraverserInBackwardsCompatibleWay(traversalDescription, layerNode).nodes()) { + previousGeomNode = node; + } + } + if (previousGeomNode != null) { + previousGeomNode.createRelationshipTo(geomNode, OrderedRelationshipTypes.NEXT_GEOM); + } else { + layerNode.createRelationshipTo(geomNode, OrderedRelationshipTypes.GEOMETRIES); + } + previousGeomNode = geomNode; + return geomNode; + } - /** - * Provides a method for iterating over all nodes that represent geometries in this dataset. - * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return - * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to - * generate a Geometry. There is no restricting on a node belonging to multiple datasets, or - * multiple layers within the same dataset. - * - * @return iterable over geometry nodes in the dataset - * @param tx - */ - public Iterable getAllGeometryNodes(Transaction tx) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .evaluator(Evaluators.excludeStartPosition()) - .relationships(OrderedRelationshipTypes.GEOMETRIES, Direction.OUTGOING) - .relationships(OrderedRelationshipTypes.NEXT_GEOM, Direction.OUTGOING); - return td.traverse(getLayerNode(tx)).nodes(); - } + /** + * Provides a method for iterating over all nodes that represent geometries in this dataset. + * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return + * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to + * generate a Geometry. There is no restricting on a node belonging to multiple datasets, or + * multiple layers within the same dataset. + * + * @param tx + * @return iterable over geometry nodes in the dataset + */ + public Iterable getAllGeometryNodes(Transaction tx) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .evaluator(Evaluators.excludeStartPosition()) + .relationships(OrderedRelationshipTypes.GEOMETRIES, Direction.OUTGOING) + .relationships(OrderedRelationshipTypes.NEXT_GEOM, Direction.OUTGOING); + return td.traverse(getLayerNode(tx)).nodes(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/ShapefileExporter.java b/src/main/java/org/neo4j/gis/spatial/ShapefileExporter.java index 4d50d1b01..8479c8194 100644 --- a/src/main/java/org/neo4j/gis/spatial/ShapefileExporter.java +++ b/src/main/java/org/neo4j/gis/spatial/ShapefileExporter.java @@ -24,18 +24,18 @@ import java.net.URL; import java.util.HashMap; import java.util.Map; - import org.geotools.api.data.FeatureStore; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.feature.type.GeometryDescriptor; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.data.neo4j.Neo4jSpatialDataStore; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.data.shapefile.ShapefileDataStoreFactory; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.feature.type.GeometryDescriptor; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; public class ShapefileExporter { + Neo4jSpatialDataStore neo4jDataStore; File exportDir; @@ -71,33 +71,34 @@ private File checkFile(File file) { } public File exportLayer(String layerName, File file) throws Exception { - file = checkFile(file); - ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory(); - Map create = new HashMap(); - URL url = file.toURI().toURL(); - create.put("url", url); - create.put("create spatial index", Boolean.TRUE); - create.put("charset", "UTF-8"); - ShapefileDataStore shpDataStore = (ShapefileDataStore) factory.createNewDataStore(create); - CoordinateReferenceSystem crs = null; + file = checkFile(file); + ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory(); + Map create = new HashMap(); + URL url = file.toURI().toURL(); + create.put("url", url); + create.put("create spatial index", Boolean.TRUE); + create.put("charset", "UTF-8"); + ShapefileDataStore shpDataStore = (ShapefileDataStore) factory.createNewDataStore(create); + CoordinateReferenceSystem crs = null; try (Transaction tx = neo4jDataStore.beginTx()) { SimpleFeatureType featureType = neo4jDataStore.getSchema(layerName); - GeometryDescriptor geometryType = featureType.getGeometryDescriptor(); - crs = geometryType.getCoordinateReferenceSystem(); - // crs = neo4jDataStore.getFeatureSource(layerName).getInfo().getCRS(); + GeometryDescriptor geometryType = featureType.getGeometryDescriptor(); + crs = geometryType.getCoordinateReferenceSystem(); + // crs = neo4jDataStore.getFeatureSource(layerName).getInfo().getCRS(); - shpDataStore.createSchema(featureType); - FeatureStore store = (FeatureStore) shpDataStore.getFeatureSource(); - store.addFeatures(neo4jDataStore.getFeatureSource(layerName).getFeatures()); - tx.commit(); + shpDataStore.createSchema(featureType); + FeatureStore store = (FeatureStore) shpDataStore.getFeatureSource(); + store.addFeatures(neo4jDataStore.getFeatureSource(layerName).getFeatures()); + tx.commit(); + } + if (crs != null) { + shpDataStore.forceSchemaCRS(crs); } - if (crs != null) - shpDataStore.forceSchemaCRS(crs); - if (!file.exists()) { - throw new Exception("Shapefile was not created: " + file); - } else if (file.length() < 10) { - throw new Exception("Shapefile was unexpectedly small, only " + file.length() + " bytes: " + file); - } - return file; + if (!file.exists()) { + throw new Exception("Shapefile was not created: " + file); + } else if (file.length() < 10) { + throw new Exception("Shapefile was unexpectedly small, only " + file.length() + " bytes: " + file); + } + return file; } } diff --git a/src/main/java/org/neo4j/gis/spatial/ShapefileImporter.java b/src/main/java/org/neo4j/gis/spatial/ShapefileImporter.java index db322cfe0..24e09bdb7 100644 --- a/src/main/java/org/neo4j/gis/spatial/ShapefileImporter.java +++ b/src/main/java/org/neo4j/gis/spatial/ShapefileImporter.java @@ -19,10 +19,16 @@ */ package org.neo4j.gis.spatial; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Path; -import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import org.geotools.api.referencing.FactoryException; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.data.PrjFileReader; import org.geotools.data.shapefile.dbf.DbaseFileHeader; import org.geotools.data.shapefile.dbf.DbaseFileReader; @@ -31,6 +37,9 @@ import org.geotools.data.shapefile.shp.JTSUtilities; import org.geotools.data.shapefile.shp.ShapefileReader; import org.geotools.data.shapefile.shp.ShapefileReader.Record; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.dbms.api.DatabaseManagementServiceBuilder; import org.neo4j.gis.spatial.index.IndexManager; @@ -41,241 +50,243 @@ import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import org.geotools.api.referencing.FactoryException; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; public class ShapefileImporter implements Constants { - private final int commitInterval; - private final boolean maintainGeometryOrder; - private final Listener monitor; - private final GraphDatabaseService database; - private final SpatialDatabaseService spatialDatabase; - private Envelope filterEnvelope; - - public ShapefileImporter(GraphDatabaseService database, Listener monitor, int commitInterval, boolean maintainGeometryOrder) { - this.maintainGeometryOrder = maintainGeometryOrder; - if (commitInterval < 1) { - throw new IllegalArgumentException("commitInterval must be > 0"); - } - this.commitInterval = commitInterval; - this.database = database; - this.spatialDatabase = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); - - if (monitor == null) monitor = new NullListener(); - this.monitor = monitor; - } - - public ShapefileImporter(GraphDatabaseService database, Listener monitor, int commitInterval) { - this(database, monitor, commitInterval, false); - } - - public ShapefileImporter(GraphDatabaseService database, Listener monitor) { - this(database, monitor, 1000, false); - } - - public ShapefileImporter(GraphDatabaseService database) { - this(database, null, 1000, false); - } - - public void setFilterEnvelope(Envelope filterEnvelope) { - this.filterEnvelope = filterEnvelope; - } - - public List importFile(String dataset, String layerName) throws IOException { - return importFile(dataset, layerName, Charset.defaultCharset()); - } - - public List importFile(String dataset, String layerName, Charset charset) throws IOException { - Class layerClass = maintainGeometryOrder ? OrderedEditableLayer.class : EditableLayerImpl.class; - EditableLayerImpl layer; - try (Transaction tx = database.beginTx()) { - layer = (EditableLayerImpl) spatialDatabase.getOrCreateLayer(tx, layerName, WKBGeometryEncoder.class, layerClass); - tx.commit(); - } - return importFile(dataset, layer, charset); - } - - public List importFile(String dataset, EditableLayerImpl layer, Charset charset) throws IOException { - GeometryFactory geomFactory = layer.getGeometryFactory(); - ArrayList added = new ArrayList<>(); - - long startTime = System.currentTimeMillis(); - - ShpFiles shpFiles; - try { - shpFiles = new ShpFiles(new File(dataset)); - } catch (Exception e) { - try { - shpFiles = new ShpFiles(new File(dataset + ".shp")); - } catch (Exception e2) { - throw new IllegalArgumentException("Failed to access the shapefile at either '" + dataset + "' or '" + dataset + ".shp'", e); - } - } - - ShapefileReader shpReader = new ShapefileReader(shpFiles, false, true, geomFactory); - try { - Class geometryClass = JTSUtilities.findBestGeometryClass(shpReader.getHeader().getShapeType()); - int geometryType = SpatialDatabaseService.convertJtsClassToGeometryType(geometryClass); - - // TODO ask charset to user? - DbaseFileReader dbfReader = new DbaseFileReader(shpFiles, true, charset); - try { - DbaseFileHeader dbaseFileHeader = dbfReader.getHeader(); - - String[] fieldsName = new String[dbaseFileHeader.getNumFields() + 1]; - fieldsName[0] = "ID"; - for (int i = 1; i < fieldsName.length; i++) { - fieldsName[i] = dbaseFileHeader.getFieldName(i - 1); - } - - try (var tx = database.beginTx()){ - CoordinateReferenceSystem crs = readCRS(shpFiles, shpReader); - if (crs != null) { - layer.setCoordinateReferenceSystem(tx, crs); - } - - layer.setGeometryType(tx, geometryType); - - layer.mergeExtraPropertyNames(tx, fieldsName); - tx.commit(); - } - - monitor.begin(dbaseFileHeader.getNumRecords()); - try { - Record record; - Geometry geometry; - Object[] values; - ArrayList fields = new ArrayList<>(); - int recordCounter = 0; - int filterCounter = 0; - while (shpReader.hasNext() && dbfReader.hasNext()) {; - try (var tx = database.beginTx()) { - int committedSinceLastNotification = 0; - for (int i = 0; i < commitInterval; i++) { - if (shpReader.hasNext() && dbfReader.hasNext()) { - record = shpReader.nextRecord(); - recordCounter++; - committedSinceLastNotification++; - try { - fields.clear(); - geometry = (Geometry) record.shape(); - if (filterEnvelope == null || filterEnvelope.intersects(geometry.getEnvelopeInternal())) { - values = dbfReader.readEntry(); - - //convert Date to String - //necessary because Neo4j doesn't support Date properties on nodes - for (int k = 0; k < fieldsName.length - 1; k++) { - if (values[k] instanceof Date) { - Date aux = (Date) values[k]; - values[k] = aux.toString(); - } - } - - fields.add(recordCounter); - Collections.addAll(fields, values); - if (geometry.isEmpty()) { - log("warn | found empty geometry in record " + recordCounter); - } else { - // TODO check geometry.isValid() - // ? - SpatialDatabaseRecord spatial_record = layer.add(tx, geometry, fieldsName, fields.toArray(values)); - added.add(spatial_record.getGeomNode()); - } - } else { - filterCounter++; - } - } catch (IllegalArgumentException e) { - // org.geotools.data.shapefile.shp.ShapefileReader.Record.shape() can throw this exception - log("warn | found invalid geometry: index=" + recordCounter, e); - } - } - } - monitor.worked(committedSinceLastNotification); - tx.commit(); - - log("info | inserted geometries: " + (recordCounter - filterCounter)); - if (filterCounter > 0) { - log("info | ignored " + filterCounter + "/" + recordCounter - + " geometries outside filter envelope: " + filterEnvelope); - } - } - } - } finally { - monitor.done(); - } - } finally { - dbfReader.close(); - } - } finally { - shpReader.close(); - } - - long stopTime = System.currentTimeMillis(); - log("info | elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000)); - return added; - } - - private CoordinateReferenceSystem readCRS(ShpFiles shpFiles, ShapefileReader shpReader) { - try (PrjFileReader prjReader = new PrjFileReader(shpFiles.getReadChannel(ShpFileType.PRJ, shpReader))){ - return prjReader.getCoordinateReferenceSystem(); - } catch (IOException | FactoryException e) { - e.printStackTrace(); - return null; - } - } - - private void log(String message) { - System.out.println(message); - } - - private void log(String message, Exception e) { - System.out.println(message); - e.printStackTrace(); - } - - public static void main(String[] args) throws Exception { - String neoPath; - String database; - String shpPath; - String layerName; - int commitInterval = 1000; - - if (args.length < 3 || args.length > 5) { - System.err.println("Parameters: neo4jDirectory database shapefile [layerName commitInterval]"); - System.err.println("\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); - System.exit(1); - } - - neoPath = args[0]; - database = args[1]; - shpPath = args[2]; - shpPath = shpPath.substring(0, shpPath.lastIndexOf(".")); - - if (args.length == 3) { - layerName = shpPath.substring(shpPath.lastIndexOf(File.separator) + 1); - } else if (args.length == 4) { - layerName = args[3]; - } else { - layerName = args[3]; - commitInterval = Integer.parseInt(args[4]); - } - - DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(neoPath)).build(); - GraphDatabaseService db = databases.database(database); - try { - ShapefileImporter importer = new ShapefileImporter(db, new NullListener(), commitInterval); - importer.importFile(shpPath, layerName); - } finally { - databases.shutdown(); - } - } + private final int commitInterval; + private final boolean maintainGeometryOrder; + private final Listener monitor; + private final GraphDatabaseService database; + private final SpatialDatabaseService spatialDatabase; + private Envelope filterEnvelope; + + public ShapefileImporter(GraphDatabaseService database, Listener monitor, int commitInterval, + boolean maintainGeometryOrder) { + this.maintainGeometryOrder = maintainGeometryOrder; + if (commitInterval < 1) { + throw new IllegalArgumentException("commitInterval must be > 0"); + } + this.commitInterval = commitInterval; + this.database = database; + this.spatialDatabase = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); + + if (monitor == null) { + monitor = new NullListener(); + } + this.monitor = monitor; + } + + public ShapefileImporter(GraphDatabaseService database, Listener monitor, int commitInterval) { + this(database, monitor, commitInterval, false); + } + + public ShapefileImporter(GraphDatabaseService database, Listener monitor) { + this(database, monitor, 1000, false); + } + + public ShapefileImporter(GraphDatabaseService database) { + this(database, null, 1000, false); + } + + public void setFilterEnvelope(Envelope filterEnvelope) { + this.filterEnvelope = filterEnvelope; + } + + public List importFile(String dataset, String layerName) throws IOException { + return importFile(dataset, layerName, Charset.defaultCharset()); + } + + public List importFile(String dataset, String layerName, Charset charset) throws IOException { + Class layerClass = + maintainGeometryOrder ? OrderedEditableLayer.class : EditableLayerImpl.class; + EditableLayerImpl layer; + try (Transaction tx = database.beginTx()) { + layer = (EditableLayerImpl) spatialDatabase.getOrCreateLayer(tx, layerName, WKBGeometryEncoder.class, + layerClass); + tx.commit(); + } + return importFile(dataset, layer, charset); + } + + public List importFile(String dataset, EditableLayerImpl layer, Charset charset) throws IOException { + GeometryFactory geomFactory = layer.getGeometryFactory(); + ArrayList added = new ArrayList<>(); + + long startTime = System.currentTimeMillis(); + + ShpFiles shpFiles; + try { + shpFiles = new ShpFiles(new File(dataset)); + } catch (Exception e) { + try { + shpFiles = new ShpFiles(new File(dataset + ".shp")); + } catch (Exception e2) { + throw new IllegalArgumentException( + "Failed to access the shapefile at either '" + dataset + "' or '" + dataset + ".shp'", e); + } + } + + ShapefileReader shpReader = new ShapefileReader(shpFiles, false, true, geomFactory); + try { + Class geometryClass = JTSUtilities.findBestGeometryClass( + shpReader.getHeader().getShapeType()); + int geometryType = SpatialDatabaseService.convertJtsClassToGeometryType(geometryClass); + + // TODO ask charset to user? + DbaseFileReader dbfReader = new DbaseFileReader(shpFiles, true, charset); + try { + DbaseFileHeader dbaseFileHeader = dbfReader.getHeader(); + + String[] fieldsName = new String[dbaseFileHeader.getNumFields() + 1]; + fieldsName[0] = "ID"; + for (int i = 1; i < fieldsName.length; i++) { + fieldsName[i] = dbaseFileHeader.getFieldName(i - 1); + } + + try (var tx = database.beginTx()) { + CoordinateReferenceSystem crs = readCRS(shpFiles, shpReader); + if (crs != null) { + layer.setCoordinateReferenceSystem(tx, crs); + } + + layer.setGeometryType(tx, geometryType); + + layer.mergeExtraPropertyNames(tx, fieldsName); + tx.commit(); + } + + monitor.begin(dbaseFileHeader.getNumRecords()); + try { + Record record; + Geometry geometry; + Object[] values; + ArrayList fields = new ArrayList<>(); + int recordCounter = 0; + int filterCounter = 0; + while (shpReader.hasNext() && dbfReader.hasNext()) { + ; + try (var tx = database.beginTx()) { + int committedSinceLastNotification = 0; + for (int i = 0; i < commitInterval; i++) { + if (shpReader.hasNext() && dbfReader.hasNext()) { + record = shpReader.nextRecord(); + recordCounter++; + committedSinceLastNotification++; + try { + fields.clear(); + geometry = (Geometry) record.shape(); + if (filterEnvelope == null || filterEnvelope.intersects( + geometry.getEnvelopeInternal())) { + values = dbfReader.readEntry(); + + //convert Date to String + //necessary because Neo4j doesn't support Date properties on nodes + for (int k = 0; k < fieldsName.length - 1; k++) { + if (values[k] instanceof Date) { + Date aux = (Date) values[k]; + values[k] = aux.toString(); + } + } + + fields.add(recordCounter); + Collections.addAll(fields, values); + if (geometry.isEmpty()) { + log("warn | found empty geometry in record " + recordCounter); + } else { + // TODO check geometry.isValid() + // ? + SpatialDatabaseRecord spatial_record = layer.add(tx, geometry, + fieldsName, fields.toArray(values)); + added.add(spatial_record.getGeomNode()); + } + } else { + filterCounter++; + } + } catch (IllegalArgumentException e) { + // org.geotools.data.shapefile.shp.ShapefileReader.Record.shape() can throw this exception + log("warn | found invalid geometry: index=" + recordCounter, e); + } + } + } + monitor.worked(committedSinceLastNotification); + tx.commit(); + + log("info | inserted geometries: " + (recordCounter - filterCounter)); + if (filterCounter > 0) { + log("info | ignored " + filterCounter + "/" + recordCounter + + " geometries outside filter envelope: " + filterEnvelope); + } + } + } + } finally { + monitor.done(); + } + } finally { + dbfReader.close(); + } + } finally { + shpReader.close(); + } + + long stopTime = System.currentTimeMillis(); + log("info | elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000)); + return added; + } + + private CoordinateReferenceSystem readCRS(ShpFiles shpFiles, ShapefileReader shpReader) { + try (PrjFileReader prjReader = new PrjFileReader(shpFiles.getReadChannel(ShpFileType.PRJ, shpReader))) { + return prjReader.getCoordinateReferenceSystem(); + } catch (IOException | FactoryException e) { + e.printStackTrace(); + return null; + } + } + + private void log(String message) { + System.out.println(message); + } + + private void log(String message, Exception e) { + System.out.println(message); + e.printStackTrace(); + } + + public static void main(String[] args) throws Exception { + String neoPath; + String database; + String shpPath; + String layerName; + int commitInterval = 1000; + + if (args.length < 3 || args.length > 5) { + System.err.println("Parameters: neo4jDirectory database shapefile [layerName commitInterval]"); + System.err.println( + "\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); + System.exit(1); + } + + neoPath = args[0]; + database = args[1]; + shpPath = args[2]; + shpPath = shpPath.substring(0, shpPath.lastIndexOf(".")); + + if (args.length == 3) { + layerName = shpPath.substring(shpPath.lastIndexOf(File.separator) + 1); + } else if (args.length == 4) { + layerName = args[3]; + } else { + layerName = args[3]; + commitInterval = Integer.parseInt(args[4]); + } + + DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(neoPath)).build(); + GraphDatabaseService db = databases.database(database); + try { + ShapefileImporter importer = new ShapefileImporter(db, new NullListener(), commitInterval); + importer.importFile(shpPath, layerName); + } finally { + databases.shutdown(); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/SimplePointLayer.java b/src/main/java/org/neo4j/gis/spatial/SimplePointLayer.java index 7f3f71055..4fe81fee2 100644 --- a/src/main/java/org/neo4j/gis/spatial/SimplePointLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/SimplePointLayer.java @@ -19,53 +19,52 @@ */ package org.neo4j.gis.spatial; +import java.util.List; import org.locationtech.jts.geom.Coordinate; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; import org.neo4j.gis.spatial.pipes.GeoPipeline; import org.neo4j.gis.spatial.pipes.processing.OrthodromicDistance; import org.neo4j.graphdb.Transaction; -import java.util.List; - public class SimplePointLayer extends EditableLayerImpl { - public static final int LIMIT_RESULTS = 100; + public static final int LIMIT_RESULTS = 100; - public SpatialDatabaseRecord add(Transaction tx, Coordinate coordinate) { - return add(tx, coordinate, null, null); - } + public SpatialDatabaseRecord add(Transaction tx, Coordinate coordinate) { + return add(tx, coordinate, null, null); + } - public SpatialDatabaseRecord add(Transaction tx, Coordinate coordinate, String[] fieldsName, Object[] fields) { - return add(tx, getGeometryFactory().createPoint(coordinate), fieldsName, fields); - } + public SpatialDatabaseRecord add(Transaction tx, Coordinate coordinate, String[] fieldsName, Object[] fields) { + return add(tx, getGeometryFactory().createPoint(coordinate), fieldsName, fields); + } - public SpatialDatabaseRecord add(Transaction tx, double x, double y) { - return add(tx, new Coordinate(x, y), null, null); - } + public SpatialDatabaseRecord add(Transaction tx, double x, double y) { + return add(tx, new Coordinate(x, y), null, null); + } - public SpatialDatabaseRecord add(Transaction tx, double x, double y, String[] fieldsName, Object[] fields) { - return add(tx, new Coordinate(x, y), fieldsName, fields); - } + public SpatialDatabaseRecord add(Transaction tx, double x, double y, String[] fieldsName, Object[] fields) { + return add(tx, new Coordinate(x, y), fieldsName, fields); + } - public Integer getGeometryType() { - return GTYPE_POINT; - } + public Integer getGeometryType() { + return GTYPE_POINT; + } - public List findClosestPointsTo(Transaction tx, Coordinate coordinate, double d) { - return GeoPipeline - .startNearestNeighborLatLonSearch(tx, this, coordinate, d) - .sort(OrthodromicDistance.DISTANCE).toList(); - } + public List findClosestPointsTo(Transaction tx, Coordinate coordinate, double d) { + return GeoPipeline + .startNearestNeighborLatLonSearch(tx, this, coordinate, d) + .sort(OrthodromicDistance.DISTANCE).toList(); + } - public List findClosestPointsTo(Transaction tx, Coordinate coordinate, int numberOfItemsToFind) { - return GeoPipeline - .startNearestNeighborLatLonSearch(tx, this, coordinate, 2 * numberOfItemsToFind) - .sort(OrthodromicDistance.DISTANCE).next(numberOfItemsToFind); - } + public List findClosestPointsTo(Transaction tx, Coordinate coordinate, int numberOfItemsToFind) { + return GeoPipeline + .startNearestNeighborLatLonSearch(tx, this, coordinate, 2 * numberOfItemsToFind) + .sort(OrthodromicDistance.DISTANCE).next(numberOfItemsToFind); + } - public List findClosestPointsTo(Transaction tx, Coordinate coordinate) { - return GeoPipeline - .startNearestNeighborLatLonSearch(tx, this, coordinate, 2 * LIMIT_RESULTS) - .sort(OrthodromicDistance.DISTANCE).next(LIMIT_RESULTS); - } + public List findClosestPointsTo(Transaction tx, Coordinate coordinate) { + return GeoPipeline + .startNearestNeighborLatLonSearch(tx, this, coordinate, 2 * LIMIT_RESULTS) + .sort(OrthodromicDistance.DISTANCE).next(LIMIT_RESULTS); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseException.java b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseException.java index 8557f0a2e..de8b7c18f 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseException.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseException.java @@ -27,8 +27,8 @@ public class SpatialDatabaseException extends RuntimeException { public SpatialDatabaseException(Throwable error) { super(error); - } - + } + public SpatialDatabaseException(String message, Throwable error) { super(message, error); } @@ -37,4 +37,4 @@ public SpatialDatabaseException(String message) { super(message); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseRecord.java b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseRecord.java index 3982cc675..67d2b594d 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseRecord.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseRecord.java @@ -19,175 +19,181 @@ */ package org.neo4j.gis.spatial; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.attributes.PropertyMapper; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.util.HashMap; -import java.util.Map; public class SpatialDatabaseRecord implements Constants, SpatialRecord { - private Node geomNode; - private Geometry geometry; - private final Layer layer; - - public SpatialDatabaseRecord(Layer layer, Node geomNode) { - this(layer, geomNode, null); - } - - @Override - public String getId() { - return geomNode.getElementId(); - } - - public String getNodeId() { - return getId(); - } - - @Override - public Node getGeomNode() { - return geomNode; - } - - /** - * If the geomNode is to be used in a different transaction than the one in which it was created, we must call this first - */ - public void refreshGeomNode(Transaction tx) { - geomNode = tx.getNodeByElementId(geomNode.getElementId()); - } - - /** - * This method returns a simple integer representation of the geometry. Some - * geometry encoders store this directly as a property of the geometry node, - * while others might store this information elsewhere in the graph, or - * deduce it from other factors of the data model. See the GeometryEncoder - * for information about mapping from the data model to the geometry. - * - * @return integer representation of a geometry - * @deprecated This method is of questionable value, since it is better to - * query the geometry object directly, outside the result - */ - public int getType() { - //TODO: Get the type from the geometryEncoder - return SpatialDatabaseService.convertJtsClassToGeometryType(getGeometry().getClass()); - } - - @Override - public Geometry getGeometry() { - if (geometry == null) - geometry = layer.getGeometryEncoder().decodeGeometry(geomNode); - return geometry; - } - - public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { - return layer.getCoordinateReferenceSystem(tx); - } - - public String getLayerName() { - return layer.getName(); - } - - /** - * Not all geometry records have the same attribute set, so we should test - * for each specific record if it contains that property. - */ - @Override - public boolean hasProperty(Transaction tx, String name) { - PropertyMapper mapper = layer.getPropertyMappingManager().getPropertyMapper(tx, name); - return mapper == null ? hasGeometryProperty(name) : hasGeometryProperty(mapper.from()); - } - - private boolean hasGeometryProperty(String name) { - return layer.getGeometryEncoder().hasAttribute(geomNode, name); - } - - @Override - public String[] getPropertyNames(Transaction tx) { - return layer.getExtraPropertyNames(tx); - } - - public Object[] getPropertyValues(Transaction tx) { - String[] names = getPropertyNames(tx); - if (names == null) return null; - Object[] values = new Object[names.length]; - for (int i = 0; i < names.length; i++) { - values[i] = getProperty(tx, names[i]); - } - return values; - } - - public Map getProperties(Transaction tx) { - Map result = new HashMap<>(); - - String[] names = getPropertyNames(tx); - for (String name : names) { - result.put(name, getProperty(tx, name)); - } - - return result; - } - - @Override - public Object getProperty(Transaction tx, String name) { - PropertyMapper mapper = layer.getPropertyMappingManager().getPropertyMapper(tx, name); - return mapper == null ? getGeometryProperty(name) : mapper.map(getGeometryProperty(mapper.from())); - } - - private Object getGeometryProperty(String name) { - return layer.getGeometryEncoder().getAttribute(geomNode, name); - } - - public void setProperty(String name, Object value) { - checkIsNotReservedProperty(name); - geomNode.setProperty(name, value); - } - - @Override - public int hashCode() { - return geomNode.getElementId().hashCode(); - } - - @Override - public boolean equals(Object anotherObject) { - if (!(anotherObject instanceof SpatialDatabaseRecord anotherRecord)) return false; - - return Objects.equals(getNodeId(), anotherRecord.getNodeId()); - } - - @Override - public String toString() { - return "SpatialDatabaseRecord[" + getNodeId() + "]: type='" + getType() + "', props[" + getPropString() + "]"; - } - - - // Protected Constructors - - protected SpatialDatabaseRecord(Layer layer, Node geomNode, Geometry geometry) { - this.layer = layer; - this.geomNode = geomNode; - this.geometry = geometry; - } - - - // Private methods - - private void checkIsNotReservedProperty(String name) { - for (String property : RESERVED_PROPS) { - if (property.equals(name)) { - throw new SpatialDatabaseException("Updating not allowed for Reserved Property: " + name); - } - } - } - - private String getPropString() { - StringBuilder text = new StringBuilder(); - for (String key : geomNode.getPropertyKeys()) { - if (text.length() > 0) text.append(", "); - text.append(key).append(": ").append(geomNode.getProperty(key).toString()); - } - return text.toString(); - } + + private Node geomNode; + private Geometry geometry; + private final Layer layer; + + public SpatialDatabaseRecord(Layer layer, Node geomNode) { + this(layer, geomNode, null); + } + + @Override + public String getId() { + return geomNode.getElementId(); + } + + public String getNodeId() { + return getId(); + } + + @Override + public Node getGeomNode() { + return geomNode; + } + + /** + * If the geomNode is to be used in a different transaction than the one in which it was created, we must call this + * first + */ + public void refreshGeomNode(Transaction tx) { + geomNode = tx.getNodeByElementId(geomNode.getElementId()); + } + + /** + * This method returns a simple integer representation of the geometry. Some + * geometry encoders store this directly as a property of the geometry node, + * while others might store this information elsewhere in the graph, or + * deduce it from other factors of the data model. See the GeometryEncoder + * for information about mapping from the data model to the geometry. + * + * @return integer representation of a geometry + * @deprecated This method is of questionable value, since it is better to + * query the geometry object directly, outside the result + */ + public int getType() { + //TODO: Get the type from the geometryEncoder + return SpatialDatabaseService.convertJtsClassToGeometryType(getGeometry().getClass()); + } + + @Override + public Geometry getGeometry() { + if (geometry == null) { + geometry = layer.getGeometryEncoder().decodeGeometry(geomNode); + } + return geometry; + } + + public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { + return layer.getCoordinateReferenceSystem(tx); + } + + public String getLayerName() { + return layer.getName(); + } + + /** + * Not all geometry records have the same attribute set, so we should test + * for each specific record if it contains that property. + */ + @Override + public boolean hasProperty(Transaction tx, String name) { + PropertyMapper mapper = layer.getPropertyMappingManager().getPropertyMapper(tx, name); + return mapper == null ? hasGeometryProperty(name) : hasGeometryProperty(mapper.from()); + } + + private boolean hasGeometryProperty(String name) { + return layer.getGeometryEncoder().hasAttribute(geomNode, name); + } + + @Override + public String[] getPropertyNames(Transaction tx) { + return layer.getExtraPropertyNames(tx); + } + + public Object[] getPropertyValues(Transaction tx) { + String[] names = getPropertyNames(tx); + if (names == null) { + return null; + } + Object[] values = new Object[names.length]; + for (int i = 0; i < names.length; i++) { + values[i] = getProperty(tx, names[i]); + } + return values; + } + + public Map getProperties(Transaction tx) { + Map result = new HashMap<>(); + + String[] names = getPropertyNames(tx); + for (String name : names) { + result.put(name, getProperty(tx, name)); + } + + return result; + } + + @Override + public Object getProperty(Transaction tx, String name) { + PropertyMapper mapper = layer.getPropertyMappingManager().getPropertyMapper(tx, name); + return mapper == null ? getGeometryProperty(name) : mapper.map(getGeometryProperty(mapper.from())); + } + + private Object getGeometryProperty(String name) { + return layer.getGeometryEncoder().getAttribute(geomNode, name); + } + + public void setProperty(String name, Object value) { + checkIsNotReservedProperty(name); + geomNode.setProperty(name, value); + } + + @Override + public int hashCode() { + return geomNode.getElementId().hashCode(); + } + + @Override + public boolean equals(Object anotherObject) { + if (!(anotherObject instanceof SpatialDatabaseRecord anotherRecord)) { + return false; + } + + return Objects.equals(getNodeId(), anotherRecord.getNodeId()); + } + + @Override + public String toString() { + return "SpatialDatabaseRecord[" + getNodeId() + "]: type='" + getType() + "', props[" + getPropString() + "]"; + } + + // Protected Constructors + + protected SpatialDatabaseRecord(Layer layer, Node geomNode, Geometry geometry) { + this.layer = layer; + this.geomNode = geomNode; + this.geometry = geometry; + } + + // Private methods + + private void checkIsNotReservedProperty(String name) { + for (String property : RESERVED_PROPS) { + if (property.equals(name)) { + throw new SpatialDatabaseException("Updating not allowed for Reserved Property: " + name); + } + } + } + + private String getPropString() { + StringBuilder text = new StringBuilder(); + for (String key : geomNode.getPropertyKeys()) { + if (text.length() > 0) { + text.append(", "); + } + text.append(key).append(": ").append(geomNode.getProperty(key).toString()); + } + return text.toString(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java index 12caef3d1..2cb604f91 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialDatabaseService.java @@ -61,484 +61,516 @@ */ public class SpatialDatabaseService implements Constants { - public final IndexManager indexManager; - - public SpatialDatabaseService(IndexManager indexManager) { - this.indexManager = indexManager; - } - - public static void assertNotOldModel(Transaction tx) { - Node oldReferenceNode = ReferenceNodes.findDeprecatedReferenceNode(tx, "spatial_root"); - if (oldReferenceNode != null) { - throw new IllegalStateException("Old reference node exists - please upgrade the spatial database to the new format"); - } - } - - public List upgradeFromOldModel(Transaction tx) { - ArrayList layersConverted = new ArrayList<>(); - Node oldReferenceNode = ReferenceNodes.findDeprecatedReferenceNode(tx, "spatial_root"); - if (oldReferenceNode != null) { - List layers = new ArrayList<>(); - - try (var relationships = oldReferenceNode.getRelationships(Direction.OUTGOING, SpatialRelationshipTypes.LAYER)) { - for (Relationship relationship : relationships) { - layers.add(relationship.getEndNode()); - } - } - - for (Node layer : layers) { - Relationship fromRoot = layer.getSingleRelationship(SpatialRelationshipTypes.LAYER, Direction.INCOMING); - fromRoot.delete(); - layer.addLabel(LABEL_LAYER); - layersConverted.add((String) layer.getProperty(PROP_LAYER)); - } - - try (var relationships = oldReferenceNode.getRelationships()) { - if (relationships.iterator().hasNext()) { - throw new IllegalStateException("Cannot upgrade - ReferenceNode 'spatial_root' still has relationships other than layers"); - } - } - - oldReferenceNode.delete(); - } - indexManager.makeIndexFor(tx, "SpatialLayers", LABEL_LAYER, PROP_LAYER); - return layersConverted; - } - - public String[] getLayerNames(Transaction tx) { - assertNotOldModel(tx); - List names = new ArrayList<>(); - - try (var layers = tx.findNodes(LABEL_LAYER)) { - while (layers.hasNext()) { - Layer layer = LayerUtilities.makeLayerFromNode(tx, indexManager, layers.next()); - if (layer instanceof DynamicLayer) { - names.addAll(((DynamicLayer) layer).getLayerNames(tx)); - } else { - names.add(layer.getName()); - } - } - } - - return names.toArray(new String[0]); - } - - public Layer getLayer(Transaction tx, String name) { - assertNotOldModel(tx); - try (var layers = tx.findNodes(LABEL_LAYER)) { - while (layers.hasNext()) { - Node node = layers.next(); - if (name.equals(node.getProperty(PROP_LAYER))) { - return LayerUtilities.makeLayerFromNode(tx, indexManager, node); - } - } - } - return getDynamicLayer(tx, name); - } - - public Layer getDynamicLayer(Transaction tx, String name) { - assertNotOldModel(tx); - ArrayList dynamicLayers = new ArrayList<>(); - try (var layers = tx.findNodes(LABEL_LAYER)) { - while (layers.hasNext()) { - Node node = layers.next(); - if (!node.getProperty(PROP_LAYER_CLASS, "").toString().startsWith("DefaultLayer")) { - Layer layer = LayerUtilities.makeLayerFromNode(tx, indexManager, node); - if (layer instanceof DynamicLayer) { - dynamicLayers.add((DynamicLayer) LayerUtilities.makeLayerFromNode(tx, indexManager, node)); - } - } - } - } - for (DynamicLayer layer : dynamicLayers) { - for (String dynLayerName : layer.getLayerNames(tx)) { - if (name.equals(dynLayerName)) { - return layer.getLayer(tx, dynLayerName); - } - } - } - return null; - } - - /** - * Convert a layer into a DynamicLayer. This will expose the ability to add - * views, or 'dynamic layers' to the layer. - * - * @return new DynamicLayer version of the original layer - */ - public DynamicLayer asDynamicLayer(Transaction tx, Layer layer) { - if (layer instanceof DynamicLayer) { - return (DynamicLayer) layer; - } else { - Node node = layer.getLayerNode(tx); - node.setProperty(PROP_LAYER_CLASS, DynamicLayer.class.getCanonicalName()); - return (DynamicLayer) LayerUtilities.makeLayerFromNode(tx, indexManager, node); - } - } - - public DefaultLayer getOrCreateDefaultLayer(Transaction tx, String name) { - return (DefaultLayer) getOrCreateLayer(tx, name, WKBGeometryEncoder.class, EditableLayerImpl.class, ""); - } - - public EditableLayer getOrCreateEditableLayer(Transaction tx, String name, String format, String propertyNameConfig) { - Class geClass = WKBGeometryEncoder.class; - if (format != null && format.toUpperCase().startsWith("WKT")) { - geClass = WKTGeometryEncoder.class; - } - return (EditableLayer) getOrCreateLayer(tx, name, geClass, EditableLayerImpl.class, propertyNameConfig); - } - - public EditableLayer getOrCreateEditableLayer(Transaction tx, String name) { - return getOrCreateEditableLayer(tx, name, "WKB", ""); - } - - public EditableLayer getOrCreateEditableLayer(Transaction tx, String name, String wktProperty) { - return getOrCreateEditableLayer(tx, name, "WKT", wktProperty); - } - - public static final String RTREE_INDEX_NAME = "rtree"; - public static final String GEOHASH_INDEX_NAME = "geohash"; - - public Class resolveIndexClass(String index) { - if (index == null) { - return LayerRTreeIndex.class; - } - switch (index.toLowerCase()) { - case RTREE_INDEX_NAME: - return LayerRTreeIndex.class; - case GEOHASH_INDEX_NAME: - return LayerGeohashPointIndex.class; - case "zorder": - return LayerZOrderPointIndex.class; - case "hilbert": - return LayerHilbertPointIndex.class; - } - throw new IllegalArgumentException("Unknown index: " + index); - } - - public EditableLayer getOrCreateSimplePointLayer(Transaction tx, String name, String index, String xProperty, String yProperty) { - return getOrCreatePointLayer(tx, name, resolveIndexClass(index), SimplePointEncoder.class, xProperty, yProperty); - } - - public EditableLayer getOrCreateNativePointLayer(Transaction tx, String name, String index, String locationProperty) { - return getOrCreatePointLayer(tx, name, resolveIndexClass(index), SimplePointEncoder.class, locationProperty); - } - - public EditableLayer getOrCreatePointLayer(Transaction tx, String name, Class indexClass, Class encoderClass, String... encoderConfig) { - Layer layer = getLayer(tx, name); - if (layer == null) { - return (EditableLayer) createLayer(tx, name, encoderClass, SimplePointLayer.class, indexClass, makeEncoderConfig(encoderConfig), DefaultGeographicCRS.WGS84); - } else if (layer instanceof EditableLayer) { - return (EditableLayer) layer; - } else { - throw new SpatialDatabaseException("Existing layer '" + layer + "' is not of the expected type: " + EditableLayer.class); - } - } - - public Layer getOrCreateLayer(Transaction tx, String name, Class geometryEncoder, Class layerClass, String config) { - Layer layer = getLayer(tx, name); - if (layer == null) { - layer = createLayer(tx, name, geometryEncoder, layerClass, null, config); - } else if (!(layerClass == null || layerClass.isInstance(layer))) { - throw new SpatialDatabaseException("Existing layer '" + layer + "' is not of the expected type: " + layerClass); - } - return layer; - } - - public Layer getOrCreateLayer(Transaction tx, String name, Class geometryEncoder, Class layerClass) { - return getOrCreateLayer(tx, name, geometryEncoder, layerClass, ""); - } - - /** - * This method will find the Layer when given a geometry node that this layer contains. This method - * used to make use of knowledge of the RTree, traversing backwards up the tree to find the layer node, which is fast. However, for reasons of clean abstraction, - * this has been refactored to delegate the logic to the layer, so that each layer can do this in an - * implementation specific way. Now we simply iterate through the layers datasets and the first one - * to return true on the SpatialDataset.containsGeometryNode(Transaction,Node) method is returned. - *

- * We can consider removing this method for a few reasons: - * * It is non-deterministic if more than one layer contains the same geometry - * * None of the current code appears to use this method - * - * @param geometryNode to start search - * @return Layer object containing this geometry - */ - public Layer findLayerContainingGeometryNode(Transaction tx, Node geometryNode) { - for (String layerName : getLayerNames(tx)) { - Layer layer = getLayer(tx, layerName); - if (layer.getDataset().containsGeometryNode(tx, geometryNode)) { - return layer; - } - } - return null; - } - - private Layer getLayerFromChild(Transaction tx, Node child, RelationshipType relType) { - Relationship indexRel = child.getSingleRelationship(relType, Direction.INCOMING); - if (indexRel != null) { - Node layerNode = indexRel.getStartNode(); - if (layerNode.hasProperty(PROP_LAYER)) { - return LayerUtilities.makeLayerFromNode(tx, indexManager, layerNode); - } - } - return null; - } - - public boolean containsLayer(Transaction tx, String name) { - return getLayer(tx, name) != null; - } - - public Layer createWKBLayer(Transaction tx, String name) { - return createLayer(tx, name, WKBGeometryEncoder.class, EditableLayerImpl.class); - } - - public SimplePointLayer createSimplePointLayer(Transaction tx, String name) { - return createSimplePointLayer(tx, name, (String[]) null); - } - - public SimplePointLayer createSimplePointLayer(Transaction tx, String name, String xProperty, String yProperty) { - return createSimplePointLayer(tx, name, xProperty, yProperty, null); - } - - public SimplePointLayer createSimplePointLayer(Transaction tx, String name, String... xybProperties) { - return createPointLayer(tx, name, LayerRTreeIndex.class, SimplePointEncoder.class, xybProperties); - } - - public SimplePointLayer createNativePointLayer(Transaction tx, String name) { - return createNativePointLayer(tx, name, (String[]) null); - } - - public SimplePointLayer createNativePointLayer(Transaction tx, String name, String locationProperty, String bboxProperty) { - return createNativePointLayer(tx, name, locationProperty, bboxProperty, null); - } - - public SimplePointLayer createNativePointLayer(Transaction tx, String name, String... encoderConfig) { - return createPointLayer(tx, name, LayerRTreeIndex.class, NativePointEncoder.class, encoderConfig); - } - - public SimplePointLayer createPointLayer(Transaction tx, String name, Class indexClass, Class encoderClass, String... encoderConfig) { - return (SimplePointLayer) createLayer(tx, name, encoderClass, SimplePointLayer.class, indexClass, - makeEncoderConfig(encoderConfig), org.geotools.referencing.crs.DefaultGeographicCRS.WGS84); - } - - public String makeEncoderConfig(String... args) { - StringBuilder sb = new StringBuilder(); - if (args != null) { - for (String arg : args) { - if (arg != null) { - if (sb.length() > 0) - sb.append(":"); - sb.append(arg); - } - } - } - return sb.toString(); - } - - public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, Class layerClass) { - return createLayer(tx, name, geometryEncoderClass, layerClass, null, null); - } - - public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, - Class layerClass, Class indexClass, - String encoderConfig) { - return createLayer(tx, name, geometryEncoderClass, layerClass, indexClass, encoderConfig, null); - } - - public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, - Class layerClass, Class indexClass, - String encoderConfig, CoordinateReferenceSystem crs) { - if (containsLayer(tx, name)) - throw new SpatialDatabaseException("Layer " + name + " already exists"); - - Layer layer = LayerUtilities.makeLayerAndNode(tx, indexManager, name, geometryEncoderClass, layerClass, indexClass); - if (encoderConfig != null && encoderConfig.length() > 0) { - GeometryEncoder encoder = layer.getGeometryEncoder(); - if (encoder instanceof Configurable) { - ((Configurable) encoder).setConfiguration(encoderConfig); - layer.getLayerNode(tx).setProperty(PROP_GEOMENCODER_CONFIG, encoderConfig); - } else { - System.out.println("Warning: encoder configuration '" + encoderConfig + "' passed to non-configurable encoder: " + geometryEncoderClass); - } - } - if (crs != null && layer instanceof EditableLayer) { - ((EditableLayer) layer).setCoordinateReferenceSystem(tx, crs); - } - return layer; - } - - public void deleteLayer(Transaction tx, String name, Listener monitor) { - Layer layer = getLayer(tx, name); - if (layer == null) throw new SpatialDatabaseException("Layer " + name + " does not exist"); - layer.delete(tx, monitor); - } - - public static int convertGeometryNameToType(String geometryName) { - if (geometryName == null) return GTYPE_GEOMETRY; - try { - return convertJtsClassToGeometryType((Class) Class.forName("org.locationtech.jts.geom." + geometryName)); - } catch (ClassNotFoundException e) { - System.err.println("Unrecognized geometry '" + geometryName + "': " + e); - return GTYPE_GEOMETRY; - } - } - - public static String convertGeometryTypeToName(Integer geometryType) { - return convertGeometryTypeToJtsClass(geometryType).getName().replace("org.locationtech.jts.geom.", ""); - } - - public static Class convertGeometryTypeToJtsClass(Integer geometryType) { - switch (geometryType) { - case GTYPE_POINT: - return Point.class; - case GTYPE_LINESTRING: - return LineString.class; - case GTYPE_POLYGON: - return Polygon.class; - case GTYPE_MULTIPOINT: - return MultiPoint.class; - case GTYPE_MULTILINESTRING: - return MultiLineString.class; - case GTYPE_MULTIPOLYGON: - return MultiPolygon.class; - default: - return Geometry.class; - } - } - - public static int convertJtsClassToGeometryType(Class jtsClass) { - if (jtsClass.equals(Point.class)) { - return GTYPE_POINT; - } else if (jtsClass.equals(LineString.class)) { - return GTYPE_LINESTRING; - } else if (jtsClass.equals(Polygon.class)) { - return GTYPE_POLYGON; - } else if (jtsClass.equals(MultiPoint.class)) { - return GTYPE_MULTIPOINT; - } else if (jtsClass.equals(MultiLineString.class)) { - return GTYPE_MULTILINESTRING; - } else if (jtsClass.equals(MultiPolygon.class)) { - return GTYPE_MULTIPOLYGON; - } else { - return GTYPE_GEOMETRY; - } - } - - /** - * Create a new layer from the results of a previous query. This actually - * copies the resulting geometries and their attributes into entirely new - * geometries using WKBGeometryEncoder. This means it is independent of the - * format of the original data. As a consequence it will have lost any - * domain specific capabilities of the original graph, if any. Use it only - * if you want a copy of the geometries themselves, and nothing more. One - * common use case would be to create a temporary layer of the results of a - * query than you wish to now export to a format that only supports - * geometries, like Shapefile, or the PNG images produced by the - * ImageExporter. - * - * @param layerName name of new layer to create - * @param results collection of SpatialDatabaseRecords to add to new layer - * @return new Layer with copy of all geometries - */ - public Layer createResultsLayer(Transaction tx, String layerName, List results) { - EditableLayer layer = (EditableLayer) createWKBLayer(tx, layerName); - for (SpatialDatabaseRecord record : results) { - layer.add(tx, record.getGeometry()); - } - return layer; - } - - - /** - * Support mapping a String (ex: 'SimplePoint') to the respective GeometryEncoder and Layer classes - * to allow for more streamlined method for creating Layers - * This was added to help support Spatial Cypher project. - */ - public static class RegisteredLayerType { - String typeName; - Class geometryEncoder; - Class layerClass; - Class layerIndexClass; - String defaultConfig; - org.geotools.referencing.crs.AbstractCRS crs; - - RegisteredLayerType(String typeName, Class geometryEncoder, - Class layerClass, AbstractCRS crs, - Class layerIndexClass, String defaultConfig) { - this.typeName = typeName; - this.geometryEncoder = geometryEncoder; - this.layerClass = layerClass; - this.layerIndexClass = layerIndexClass; - this.crs = crs; - this.defaultConfig = defaultConfig; - } - - /** - * For external expression of the configuration of this geometry encoder - * - * @return descriptive signature of encoder, type and configuration - */ - String getSignature() { - return "RegisteredLayerType(name='" + typeName + "', geometryEncoder=" + - geometryEncoder.getSimpleName() + ", layerClass=" + layerClass.getSimpleName() + - ", index=" + layerIndexClass.getSimpleName() + - ", crs='" + crs.getName(null) + "', defaultConfig='" + defaultConfig + "')"; - } - } - - private static final Map registeredLayerTypes = new LinkedHashMap<>(); - - static { - addRegisteredLayerType(new RegisteredLayerType("SimplePoint", SimplePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "longitude:latitude")); - addRegisteredLayerType(new RegisteredLayerType("Geohash", SimplePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, "longitude:latitude")); - addRegisteredLayerType(new RegisteredLayerType("ZOrder", SimplePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerZOrderPointIndex.class, "longitude:latitude")); - addRegisteredLayerType(new RegisteredLayerType("Hilbert", SimplePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerHilbertPointIndex.class, "longitude:latitude")); - addRegisteredLayerType(new RegisteredLayerType("NativePoint", NativePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "location")); - addRegisteredLayerType(new RegisteredLayerType("NativeGeohash", NativePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, "location")); - addRegisteredLayerType(new RegisteredLayerType("NativeZOrder", NativePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerZOrderPointIndex.class, "location")); - addRegisteredLayerType(new RegisteredLayerType("NativeHilbert", NativePointEncoder.class, - SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerHilbertPointIndex.class, "location")); - addRegisteredLayerType(new RegisteredLayerType("WKT", WKTGeometryEncoder.class, EditableLayerImpl.class, - DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); - addRegisteredLayerType(new RegisteredLayerType("WKB", WKBGeometryEncoder.class, EditableLayerImpl.class, - DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); - addRegisteredLayerType(new RegisteredLayerType("OSM", OSMGeometryEncoder.class, OSMLayer.class, - DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); - } - - private static void addRegisteredLayerType(RegisteredLayerType type) { - registeredLayerTypes.put(type.typeName.toLowerCase(), type); - } - - public Layer getOrCreateRegisteredTypeLayer(Transaction tx, String name, String type, String config) { - RegisteredLayerType registeredLayerType = registeredLayerTypes.get(type.toLowerCase()); - return getOrCreateRegisteredTypeLayer(tx, name, registeredLayerType, config); - } - - public Layer getOrCreateRegisteredTypeLayer(Transaction tx, String name, RegisteredLayerType registeredLayerType, String config) { - return getOrCreateLayer(tx, name, registeredLayerType.geometryEncoder, registeredLayerType.layerClass, - (config == null) ? registeredLayerType.defaultConfig : config); - } - - public Map getRegisteredLayerTypes() { - Map results = new LinkedHashMap<>(); - registeredLayerTypes.forEach((s, definition) -> results.put(s, definition.getSignature())); - return results; - } - - public Class suggestLayerClassForEncoder(Class encoderClass) { - for (RegisteredLayerType type : registeredLayerTypes.values()) { - if (type.geometryEncoder == encoderClass) { - return type.layerClass; - } - } - return EditableLayerImpl.class; - } + public final IndexManager indexManager; + + public SpatialDatabaseService(IndexManager indexManager) { + this.indexManager = indexManager; + } + + public static void assertNotOldModel(Transaction tx) { + Node oldReferenceNode = ReferenceNodes.findDeprecatedReferenceNode(tx, "spatial_root"); + if (oldReferenceNode != null) { + throw new IllegalStateException( + "Old reference node exists - please upgrade the spatial database to the new format"); + } + } + + public List upgradeFromOldModel(Transaction tx) { + ArrayList layersConverted = new ArrayList<>(); + Node oldReferenceNode = ReferenceNodes.findDeprecatedReferenceNode(tx, "spatial_root"); + if (oldReferenceNode != null) { + List layers = new ArrayList<>(); + + try (var relationships = oldReferenceNode.getRelationships(Direction.OUTGOING, + SpatialRelationshipTypes.LAYER)) { + for (Relationship relationship : relationships) { + layers.add(relationship.getEndNode()); + } + } + + for (Node layer : layers) { + Relationship fromRoot = layer.getSingleRelationship(SpatialRelationshipTypes.LAYER, Direction.INCOMING); + fromRoot.delete(); + layer.addLabel(LABEL_LAYER); + layersConverted.add((String) layer.getProperty(PROP_LAYER)); + } + + try (var relationships = oldReferenceNode.getRelationships()) { + if (relationships.iterator().hasNext()) { + throw new IllegalStateException( + "Cannot upgrade - ReferenceNode 'spatial_root' still has relationships other than layers"); + } + } + + oldReferenceNode.delete(); + } + indexManager.makeIndexFor(tx, "SpatialLayers", LABEL_LAYER, PROP_LAYER); + return layersConverted; + } + + public String[] getLayerNames(Transaction tx) { + assertNotOldModel(tx); + List names = new ArrayList<>(); + + try (var layers = tx.findNodes(LABEL_LAYER)) { + while (layers.hasNext()) { + Layer layer = LayerUtilities.makeLayerFromNode(tx, indexManager, layers.next()); + if (layer instanceof DynamicLayer) { + names.addAll(((DynamicLayer) layer).getLayerNames(tx)); + } else { + names.add(layer.getName()); + } + } + } + + return names.toArray(new String[0]); + } + + public Layer getLayer(Transaction tx, String name) { + assertNotOldModel(tx); + try (var layers = tx.findNodes(LABEL_LAYER)) { + while (layers.hasNext()) { + Node node = layers.next(); + if (name.equals(node.getProperty(PROP_LAYER))) { + return LayerUtilities.makeLayerFromNode(tx, indexManager, node); + } + } + } + return getDynamicLayer(tx, name); + } + + public Layer getDynamicLayer(Transaction tx, String name) { + assertNotOldModel(tx); + ArrayList dynamicLayers = new ArrayList<>(); + try (var layers = tx.findNodes(LABEL_LAYER)) { + while (layers.hasNext()) { + Node node = layers.next(); + if (!node.getProperty(PROP_LAYER_CLASS, "").toString().startsWith("DefaultLayer")) { + Layer layer = LayerUtilities.makeLayerFromNode(tx, indexManager, node); + if (layer instanceof DynamicLayer) { + dynamicLayers.add((DynamicLayer) LayerUtilities.makeLayerFromNode(tx, indexManager, node)); + } + } + } + } + for (DynamicLayer layer : dynamicLayers) { + for (String dynLayerName : layer.getLayerNames(tx)) { + if (name.equals(dynLayerName)) { + return layer.getLayer(tx, dynLayerName); + } + } + } + return null; + } + + /** + * Convert a layer into a DynamicLayer. This will expose the ability to add + * views, or 'dynamic layers' to the layer. + * + * @return new DynamicLayer version of the original layer + */ + public DynamicLayer asDynamicLayer(Transaction tx, Layer layer) { + if (layer instanceof DynamicLayer) { + return (DynamicLayer) layer; + } else { + Node node = layer.getLayerNode(tx); + node.setProperty(PROP_LAYER_CLASS, DynamicLayer.class.getCanonicalName()); + return (DynamicLayer) LayerUtilities.makeLayerFromNode(tx, indexManager, node); + } + } + + public DefaultLayer getOrCreateDefaultLayer(Transaction tx, String name) { + return (DefaultLayer) getOrCreateLayer(tx, name, WKBGeometryEncoder.class, EditableLayerImpl.class, ""); + } + + public EditableLayer getOrCreateEditableLayer(Transaction tx, String name, String format, + String propertyNameConfig) { + Class geClass = WKBGeometryEncoder.class; + if (format != null && format.toUpperCase().startsWith("WKT")) { + geClass = WKTGeometryEncoder.class; + } + return (EditableLayer) getOrCreateLayer(tx, name, geClass, EditableLayerImpl.class, propertyNameConfig); + } + + public EditableLayer getOrCreateEditableLayer(Transaction tx, String name) { + return getOrCreateEditableLayer(tx, name, "WKB", ""); + } + + public EditableLayer getOrCreateEditableLayer(Transaction tx, String name, String wktProperty) { + return getOrCreateEditableLayer(tx, name, "WKT", wktProperty); + } + + public static final String RTREE_INDEX_NAME = "rtree"; + public static final String GEOHASH_INDEX_NAME = "geohash"; + + public Class resolveIndexClass(String index) { + if (index == null) { + return LayerRTreeIndex.class; + } + switch (index.toLowerCase()) { + case RTREE_INDEX_NAME: + return LayerRTreeIndex.class; + case GEOHASH_INDEX_NAME: + return LayerGeohashPointIndex.class; + case "zorder": + return LayerZOrderPointIndex.class; + case "hilbert": + return LayerHilbertPointIndex.class; + } + throw new IllegalArgumentException("Unknown index: " + index); + } + + public EditableLayer getOrCreateSimplePointLayer(Transaction tx, String name, String index, String xProperty, + String yProperty) { + return getOrCreatePointLayer(tx, name, resolveIndexClass(index), SimplePointEncoder.class, xProperty, + yProperty); + } + + public EditableLayer getOrCreateNativePointLayer(Transaction tx, String name, String index, + String locationProperty) { + return getOrCreatePointLayer(tx, name, resolveIndexClass(index), SimplePointEncoder.class, locationProperty); + } + + public EditableLayer getOrCreatePointLayer(Transaction tx, String name, + Class indexClass, Class encoderClass, + String... encoderConfig) { + Layer layer = getLayer(tx, name); + if (layer == null) { + return (EditableLayer) createLayer(tx, name, encoderClass, SimplePointLayer.class, indexClass, + makeEncoderConfig(encoderConfig), DefaultGeographicCRS.WGS84); + } else if (layer instanceof EditableLayer) { + return (EditableLayer) layer; + } else { + throw new SpatialDatabaseException( + "Existing layer '" + layer + "' is not of the expected type: " + EditableLayer.class); + } + } + + public Layer getOrCreateLayer(Transaction tx, String name, Class geometryEncoder, + Class layerClass, String config) { + Layer layer = getLayer(tx, name); + if (layer == null) { + layer = createLayer(tx, name, geometryEncoder, layerClass, null, config); + } else if (!(layerClass == null || layerClass.isInstance(layer))) { + throw new SpatialDatabaseException( + "Existing layer '" + layer + "' is not of the expected type: " + layerClass); + } + return layer; + } + + public Layer getOrCreateLayer(Transaction tx, String name, Class geometryEncoder, + Class layerClass) { + return getOrCreateLayer(tx, name, geometryEncoder, layerClass, ""); + } + + /** + * This method will find the Layer when given a geometry node that this layer contains. This method + * used to make use of knowledge of the RTree, traversing backwards up the tree to find the layer node, which is + * fast. However, for reasons of clean abstraction, + * this has been refactored to delegate the logic to the layer, so that each layer can do this in an + * implementation specific way. Now we simply iterate through the layers datasets and the first one + * to return true on the SpatialDataset.containsGeometryNode(Transaction,Node) method is returned. + *

+ * We can consider removing this method for a few reasons: + * * It is non-deterministic if more than one layer contains the same geometry + * * None of the current code appears to use this method + * + * @param geometryNode to start search + * @return Layer object containing this geometry + */ + public Layer findLayerContainingGeometryNode(Transaction tx, Node geometryNode) { + for (String layerName : getLayerNames(tx)) { + Layer layer = getLayer(tx, layerName); + if (layer.getDataset().containsGeometryNode(tx, geometryNode)) { + return layer; + } + } + return null; + } + + private Layer getLayerFromChild(Transaction tx, Node child, RelationshipType relType) { + Relationship indexRel = child.getSingleRelationship(relType, Direction.INCOMING); + if (indexRel != null) { + Node layerNode = indexRel.getStartNode(); + if (layerNode.hasProperty(PROP_LAYER)) { + return LayerUtilities.makeLayerFromNode(tx, indexManager, layerNode); + } + } + return null; + } + + public boolean containsLayer(Transaction tx, String name) { + return getLayer(tx, name) != null; + } + + public Layer createWKBLayer(Transaction tx, String name) { + return createLayer(tx, name, WKBGeometryEncoder.class, EditableLayerImpl.class); + } + + public SimplePointLayer createSimplePointLayer(Transaction tx, String name) { + return createSimplePointLayer(tx, name, (String[]) null); + } + + public SimplePointLayer createSimplePointLayer(Transaction tx, String name, String xProperty, String yProperty) { + return createSimplePointLayer(tx, name, xProperty, yProperty, null); + } + + public SimplePointLayer createSimplePointLayer(Transaction tx, String name, String... xybProperties) { + return createPointLayer(tx, name, LayerRTreeIndex.class, SimplePointEncoder.class, xybProperties); + } + + public SimplePointLayer createNativePointLayer(Transaction tx, String name) { + return createNativePointLayer(tx, name, (String[]) null); + } + + public SimplePointLayer createNativePointLayer(Transaction tx, String name, String locationProperty, + String bboxProperty) { + return createNativePointLayer(tx, name, locationProperty, bboxProperty, null); + } + + public SimplePointLayer createNativePointLayer(Transaction tx, String name, String... encoderConfig) { + return createPointLayer(tx, name, LayerRTreeIndex.class, NativePointEncoder.class, encoderConfig); + } + + public SimplePointLayer createPointLayer(Transaction tx, String name, Class indexClass, + Class encoderClass, String... encoderConfig) { + return (SimplePointLayer) createLayer(tx, name, encoderClass, SimplePointLayer.class, indexClass, + makeEncoderConfig(encoderConfig), org.geotools.referencing.crs.DefaultGeographicCRS.WGS84); + } + + public String makeEncoderConfig(String... args) { + StringBuilder sb = new StringBuilder(); + if (args != null) { + for (String arg : args) { + if (arg != null) { + if (sb.length() > 0) { + sb.append(":"); + } + sb.append(arg); + } + } + } + return sb.toString(); + } + + public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, + Class layerClass) { + return createLayer(tx, name, geometryEncoderClass, layerClass, null, null); + } + + public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, + Class layerClass, Class indexClass, + String encoderConfig) { + return createLayer(tx, name, geometryEncoderClass, layerClass, indexClass, encoderConfig, null); + } + + public Layer createLayer(Transaction tx, String name, Class geometryEncoderClass, + Class layerClass, Class indexClass, + String encoderConfig, CoordinateReferenceSystem crs) { + if (containsLayer(tx, name)) { + throw new SpatialDatabaseException("Layer " + name + " already exists"); + } + + Layer layer = LayerUtilities.makeLayerAndNode(tx, indexManager, name, geometryEncoderClass, layerClass, + indexClass); + if (encoderConfig != null && encoderConfig.length() > 0) { + GeometryEncoder encoder = layer.getGeometryEncoder(); + if (encoder instanceof Configurable) { + ((Configurable) encoder).setConfiguration(encoderConfig); + layer.getLayerNode(tx).setProperty(PROP_GEOMENCODER_CONFIG, encoderConfig); + } else { + System.out.println( + "Warning: encoder configuration '" + encoderConfig + "' passed to non-configurable encoder: " + + geometryEncoderClass); + } + } + if (crs != null && layer instanceof EditableLayer) { + ((EditableLayer) layer).setCoordinateReferenceSystem(tx, crs); + } + return layer; + } + + public void deleteLayer(Transaction tx, String name, Listener monitor) { + Layer layer = getLayer(tx, name); + if (layer == null) { + throw new SpatialDatabaseException("Layer " + name + " does not exist"); + } + layer.delete(tx, monitor); + } + + public static int convertGeometryNameToType(String geometryName) { + if (geometryName == null) { + return GTYPE_GEOMETRY; + } + try { + return convertJtsClassToGeometryType( + (Class) Class.forName("org.locationtech.jts.geom." + geometryName)); + } catch (ClassNotFoundException e) { + System.err.println("Unrecognized geometry '" + geometryName + "': " + e); + return GTYPE_GEOMETRY; + } + } + + public static String convertGeometryTypeToName(Integer geometryType) { + return convertGeometryTypeToJtsClass(geometryType).getName().replace("org.locationtech.jts.geom.", ""); + } + + public static Class convertGeometryTypeToJtsClass(Integer geometryType) { + switch (geometryType) { + case GTYPE_POINT: + return Point.class; + case GTYPE_LINESTRING: + return LineString.class; + case GTYPE_POLYGON: + return Polygon.class; + case GTYPE_MULTIPOINT: + return MultiPoint.class; + case GTYPE_MULTILINESTRING: + return MultiLineString.class; + case GTYPE_MULTIPOLYGON: + return MultiPolygon.class; + default: + return Geometry.class; + } + } + + public static int convertJtsClassToGeometryType(Class jtsClass) { + if (jtsClass.equals(Point.class)) { + return GTYPE_POINT; + } else if (jtsClass.equals(LineString.class)) { + return GTYPE_LINESTRING; + } else if (jtsClass.equals(Polygon.class)) { + return GTYPE_POLYGON; + } else if (jtsClass.equals(MultiPoint.class)) { + return GTYPE_MULTIPOINT; + } else if (jtsClass.equals(MultiLineString.class)) { + return GTYPE_MULTILINESTRING; + } else if (jtsClass.equals(MultiPolygon.class)) { + return GTYPE_MULTIPOLYGON; + } else { + return GTYPE_GEOMETRY; + } + } + + /** + * Create a new layer from the results of a previous query. This actually + * copies the resulting geometries and their attributes into entirely new + * geometries using WKBGeometryEncoder. This means it is independent of the + * format of the original data. As a consequence it will have lost any + * domain specific capabilities of the original graph, if any. Use it only + * if you want a copy of the geometries themselves, and nothing more. One + * common use case would be to create a temporary layer of the results of a + * query than you wish to now export to a format that only supports + * geometries, like Shapefile, or the PNG images produced by the + * ImageExporter. + * + * @param layerName name of new layer to create + * @param results collection of SpatialDatabaseRecords to add to new layer + * @return new Layer with copy of all geometries + */ + public Layer createResultsLayer(Transaction tx, String layerName, List results) { + EditableLayer layer = (EditableLayer) createWKBLayer(tx, layerName); + for (SpatialDatabaseRecord record : results) { + layer.add(tx, record.getGeometry()); + } + return layer; + } + + + /** + * Support mapping a String (ex: 'SimplePoint') to the respective GeometryEncoder and Layer classes + * to allow for more streamlined method for creating Layers + * This was added to help support Spatial Cypher project. + */ + public static class RegisteredLayerType { + + String typeName; + Class geometryEncoder; + Class layerClass; + Class layerIndexClass; + String defaultConfig; + org.geotools.referencing.crs.AbstractCRS crs; + + RegisteredLayerType(String typeName, Class geometryEncoder, + Class layerClass, AbstractCRS crs, + Class layerIndexClass, String defaultConfig) { + this.typeName = typeName; + this.geometryEncoder = geometryEncoder; + this.layerClass = layerClass; + this.layerIndexClass = layerIndexClass; + this.crs = crs; + this.defaultConfig = defaultConfig; + } + + /** + * For external expression of the configuration of this geometry encoder + * + * @return descriptive signature of encoder, type and configuration + */ + String getSignature() { + return "RegisteredLayerType(name='" + typeName + "', geometryEncoder=" + + geometryEncoder.getSimpleName() + ", layerClass=" + layerClass.getSimpleName() + + ", index=" + layerIndexClass.getSimpleName() + + ", crs='" + crs.getName(null) + "', defaultConfig='" + defaultConfig + "')"; + } + } + + private static final Map registeredLayerTypes = new LinkedHashMap<>(); + + static { + addRegisteredLayerType(new RegisteredLayerType("SimplePoint", SimplePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "longitude:latitude")); + addRegisteredLayerType(new RegisteredLayerType("Geohash", SimplePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, + "longitude:latitude")); + addRegisteredLayerType(new RegisteredLayerType("ZOrder", SimplePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerZOrderPointIndex.class, "longitude:latitude")); + addRegisteredLayerType(new RegisteredLayerType("Hilbert", SimplePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerHilbertPointIndex.class, + "longitude:latitude")); + addRegisteredLayerType(new RegisteredLayerType("NativePoint", NativePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "location")); + addRegisteredLayerType(new RegisteredLayerType("NativeGeohash", NativePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerGeohashPointIndex.class, "location")); + addRegisteredLayerType(new RegisteredLayerType("NativeZOrder", NativePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerZOrderPointIndex.class, "location")); + addRegisteredLayerType(new RegisteredLayerType("NativeHilbert", NativePointEncoder.class, + SimplePointLayer.class, DefaultGeographicCRS.WGS84, LayerHilbertPointIndex.class, "location")); + addRegisteredLayerType(new RegisteredLayerType("WKT", WKTGeometryEncoder.class, EditableLayerImpl.class, + DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); + addRegisteredLayerType(new RegisteredLayerType("WKB", WKBGeometryEncoder.class, EditableLayerImpl.class, + DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); + addRegisteredLayerType(new RegisteredLayerType("OSM", OSMGeometryEncoder.class, OSMLayer.class, + DefaultGeographicCRS.WGS84, LayerRTreeIndex.class, "geometry")); + } + + private static void addRegisteredLayerType(RegisteredLayerType type) { + registeredLayerTypes.put(type.typeName.toLowerCase(), type); + } + + public Layer getOrCreateRegisteredTypeLayer(Transaction tx, String name, String type, String config) { + RegisteredLayerType registeredLayerType = registeredLayerTypes.get(type.toLowerCase()); + return getOrCreateRegisteredTypeLayer(tx, name, registeredLayerType, config); + } + + public Layer getOrCreateRegisteredTypeLayer(Transaction tx, String name, RegisteredLayerType registeredLayerType, + String config) { + return getOrCreateLayer(tx, name, registeredLayerType.geometryEncoder, registeredLayerType.layerClass, + (config == null) ? registeredLayerType.defaultConfig : config); + } + + public Map getRegisteredLayerTypes() { + Map results = new LinkedHashMap<>(); + registeredLayerTypes.forEach((s, definition) -> results.put(s, definition.getSignature())); + return results; + } + + public Class suggestLayerClassForEncoder(Class encoderClass) { + for (RegisteredLayerType type : registeredLayerTypes.values()) { + if (type.geometryEncoder == encoderClass) { + return type.layerClass; + } + } + return EditableLayerImpl.class; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialDataset.java b/src/main/java/org/neo4j/gis/spatial/SpatialDataset.java index 1b8569132..8fc4034ff 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialDataset.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialDataset.java @@ -19,9 +19,8 @@ */ package org.neo4j.gis.spatial; -import org.neo4j.graphdb.Node; - import org.locationtech.jts.geom.Geometry; +import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; /** @@ -39,46 +38,46 @@ */ public interface SpatialDataset { - /** - * Provides a method for iterating over all nodes that represent geometries in this dataset. - * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return - * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to - * generate a Geometry. There is no restriction on a node belonging to multiple datasets, or - * multiple layers within the same dataset. - * - * @return iterable over geometry nodes in the dataset - * @param tx - */ - public Iterable getAllGeometryNodes(Transaction tx); + /** + * Provides a method for iterating over all nodes that represent geometries in this dataset. + * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return + * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to + * generate a Geometry. There is no restriction on a node belonging to multiple datasets, or + * multiple layers within the same dataset. + * + * @param tx + * @return iterable over geometry nodes in the dataset + */ + public Iterable getAllGeometryNodes(Transaction tx); - /** - * Provides a method for iterating over all geometries in this dataset. This is similar to the - * getAllGeometryNodes() method but internally converts the Node to a Geometry. - * - * @return iterable over geometries in the dataset - */ - public Iterable< ? extends Geometry> getAllGeometries(Transaction tx); + /** + * Provides a method for iterating over all geometries in this dataset. This is similar to the + * getAllGeometryNodes() method but internally converts the Node to a Geometry. + * + * @return iterable over geometries in the dataset + */ + public Iterable getAllGeometries(Transaction tx); - /** - * Return the geometry encoder used by this SpatialDataset to convert individual geometries to - * and from the database structure. - * - * @return GeometryEncoder for this dataset - */ - public GeometryEncoder getGeometryEncoder(); + /** + * Return the geometry encoder used by this SpatialDataset to convert individual geometries to + * and from the database structure. + * + * @return GeometryEncoder for this dataset + */ + public GeometryEncoder getGeometryEncoder(); - /** - * Each dataset can have one or more layers. This methods provides a way to iterate over all - * layers. - * - * @return iterable over all Layers that can be viewed from this dataset - */ - public Iterable< ? extends Layer> getLayers(); + /** + * Each dataset can have one or more layers. This methods provides a way to iterate over all + * layers. + * + * @return iterable over all Layers that can be viewed from this dataset + */ + public Iterable getLayers(); - /** - * Does the dataset (or layer) contain the geometry specified by this node. - * - * @return boolean true/false if the geometry node is in this Dataset or Layer - */ - public boolean containsGeometryNode(Transaction tx, Node node); + /** + * Does the dataset (or layer) contain the geometry specified by this node. + * + * @return boolean true/false if the geometry node is in this Dataset or Layer + */ + public boolean containsGeometryNode(Transaction tx, Node node); } diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialRecord.java b/src/main/java/org/neo4j/gis/spatial/SpatialRecord.java index 3b5718989..0fa406e23 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialRecord.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialRecord.java @@ -20,7 +20,6 @@ package org.neo4j.gis.spatial; import java.util.Map; - import org.locationtech.jts.geom.Geometry; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; @@ -28,7 +27,7 @@ public interface SpatialRecord { String getId(); - + Geometry getGeometry(); boolean hasProperty(Transaction tx, String name); diff --git a/src/main/java/org/neo4j/gis/spatial/SpatialTopologyUtils.java b/src/main/java/org/neo4j/gis/spatial/SpatialTopologyUtils.java index 436d58f90..8d3ecbde6 100644 --- a/src/main/java/org/neo4j/gis/spatial/SpatialTopologyUtils.java +++ b/src/main/java/org/neo4j/gis/spatial/SpatialTopologyUtils.java @@ -19,297 +19,311 @@ */ package org.neo4j.gis.spatial; -import org.locationtech.jts.geom.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.geotools.geometry.jts.ReferencedEnvelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.linearref.LengthIndexedLine; import org.locationtech.jts.linearref.LinearLocation; import org.locationtech.jts.linearref.LocationIndexedLine; -import org.geotools.geometry.jts.ReferencedEnvelope; import org.neo4j.gis.spatial.filter.SearchIntersect; import org.neo4j.graphdb.Transaction; -import java.util.*; - /** * This class is a temporary location for collecting a number of spatial * utilities before we have decided on a more complete analysis structure. Do * not rely on this API remaining constant. */ public class SpatialTopologyUtils { - /** - * Inner class associating points and resulting geometry records to - * facilitate the result set returned. - */ - public static class PointResult implements Map.Entry, Comparable { - private final Point point; - private SpatialDatabaseRecord record; - private final double distance; - private PointResult(Point point, SpatialDatabaseRecord record, double distance) { - this.point = point; - this.record = record; - this.distance = distance; - } + /** + * Inner class associating points and resulting geometry records to + * facilitate the result set returned. + */ + public static class PointResult implements Map.Entry, Comparable { + + private final Point point; + private SpatialDatabaseRecord record; + private final double distance; + + private PointResult(Point point, SpatialDatabaseRecord record, double distance) { + this.point = point; + this.record = record; + this.distance = distance; + } - public Point getKey() { - return point; - } + public Point getKey() { + return point; + } - public SpatialDatabaseRecord getValue() { - return record; - } + public SpatialDatabaseRecord getValue() { + return record; + } - public double getDistance() { - return distance; - } + public double getDistance() { + return distance; + } - public SpatialDatabaseRecord setValue(SpatialDatabaseRecord value) { - return this.record = value; - } + public SpatialDatabaseRecord setValue(SpatialDatabaseRecord value) { + return this.record = value; + } - public int compareTo(PointResult other) { - return Double.compare(this.distance, other.distance); - } + public int compareTo(PointResult other) { + return Double.compare(this.distance, other.distance); + } - public String toString() { - return "Point[" + point + "] distance[" + distance + "] record[" + record + "]"; - } - } + public String toString() { + return "Point[" + point + "] distance[" + distance + "] record[" + record + "]"; + } + } - public static List findClosestEdges(Transaction tx, Point point, Layer layer) { - return findClosestEdges(tx, point, layer, 0.0); - } + public static List findClosestEdges(Transaction tx, Point point, Layer layer) { + return findClosestEdges(tx, point, layer, 0.0); + } - public static List findClosestEdges(Transaction tx, Point point, Layer layer, double distance) { - if (layer.getIndex().isEmpty(tx)) { - return new ArrayList<>(0); - } else { - ReferencedEnvelope env = new ReferencedEnvelope( - Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)), - layer.getCoordinateReferenceSystem(tx)); - if (distance <= 0.0) - distance = env.getSpan(0) / 100.0; - Envelope search = new Envelope(point.getCoordinate()); - search.expandBy(distance); - GeometryFactory factory = layer.getGeometryFactory(); - return findClosestEdges(tx, point, layer, factory.toGeometry(search)); - } - } + public static List findClosestEdges(Transaction tx, Point point, Layer layer, double distance) { + if (layer.getIndex().isEmpty(tx)) { + return new ArrayList<>(0); + } else { + ReferencedEnvelope env = new ReferencedEnvelope( + Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)), + layer.getCoordinateReferenceSystem(tx)); + if (distance <= 0.0) { + distance = env.getSpan(0) / 100.0; + } + Envelope search = new Envelope(point.getCoordinate()); + search.expandBy(distance); + GeometryFactory factory = layer.getGeometryFactory(); + return findClosestEdges(tx, point, layer, factory.toGeometry(search)); + } + } - /** - * Find geometries in the given layer that are closest to the given point while applying the filter - * currently only handles point and linestrings (projecting them to a point) TODO Craig for other geoms - * - * @return list of point results containing the matched point on the geometry, the spatial record and the distance each - */ - public static List findClosestEdges(Transaction tx, Point point, Layer layer, Geometry filter) { - ArrayList results = new ArrayList<>(); + /** + * Find geometries in the given layer that are closest to the given point while applying the filter + * currently only handles point and linestrings (projecting them to a point) TODO Craig for other geoms + * + * @return list of point results containing the matched point on the geometry, the spatial record and the distance each + */ + public static List findClosestEdges(Transaction tx, Point point, Layer layer, Geometry filter) { + ArrayList results = new ArrayList<>(); - Iterator records = layer.getIndex().search(tx, new SearchIntersect(layer, filter)); - while (records.hasNext()) { - SpatialDatabaseRecord record = records.next(); - Geometry geom = record.getGeometry(); - if (geom instanceof LineString) { - LocationIndexedLine line = new LocationIndexedLine(geom); - LinearLocation here = line.project(point.getCoordinate()); - Coordinate snap = line.extractPoint(here); - double distance = snap.distance(point.getCoordinate()); - results.add(new PointResult(layer.getGeometryFactory() - .createPoint(snap), record, distance)); - } else if (geom instanceof Point) { - Point here = (Point) geom; - results.add(new PointResult(here, record, here.distance(point))); - } - } - Collections.sort(results); - return results; - } + Iterator records = layer.getIndex().search(tx, new SearchIntersect(layer, filter)); + while (records.hasNext()) { + SpatialDatabaseRecord record = records.next(); + Geometry geom = record.getGeometry(); + if (geom instanceof LineString) { + LocationIndexedLine line = new LocationIndexedLine(geom); + LinearLocation here = line.project(point.getCoordinate()); + Coordinate snap = line.extractPoint(here); + double distance = snap.distance(point.getCoordinate()); + results.add(new PointResult(layer.getGeometryFactory() + .createPoint(snap), record, distance)); + } else if (geom instanceof Point) { + Point here = (Point) geom; + results.add(new PointResult(here, record, here.distance(point))); + } + } + Collections.sort(results); + return results; + } - /** - * Create a Point located at the specified 'measure' distance along a - * Geometry. This is achieved through using the JTS - * LengthIndexedLine.extractPoint(measure) method for finding the - * coordinates at the specified measure along the geometry. It is equivalent - * to Oracle's SDO_LRS.LOCATE_PT. - * - * @param layer Layer the geometry is contained by, and is used to access the - * GeometryFactory for creating the Point - * @param geometry Geometry to measure - * @param measure the distance along the geometry - * @return Point at 'measure' distance along the geometry - * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 - * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html - */ - public static Point locatePoint(Layer layer, Geometry geometry, double measure) { - return layer.getGeometryFactory().createPoint(locatePoint(geometry, measure)); - } + /** + * Create a Point located at the specified 'measure' distance along a + * Geometry. This is achieved through using the JTS + * LengthIndexedLine.extractPoint(measure) method for finding the + * coordinates at the specified measure along the geometry. It is equivalent + * to Oracle's SDO_LRS.LOCATE_PT. + * + * @param layer Layer the geometry is contained by, and is used to access the + * GeometryFactory for creating the Point + * @param geometry Geometry to measure + * @param measure the distance along the geometry + * @return Point at 'measure' distance along the geometry + * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 + * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html + */ + public static Point locatePoint(Layer layer, Geometry geometry, double measure) { + return layer.getGeometryFactory().createPoint(locatePoint(geometry, measure)); + } - /** - * Find the coordinate at the specified 'measure' distance along a - * Geometry. This is achieved through using the JTS - * LengthIndexedLine.extractPoint(measure) method for finding the - * coordinates at the specified measure along the geometry. It is equivalent - * to Oracle's SDO_LRS.LOCATE_PT. - * - * @param geometry Geometry to measure - * @param measure the distance along the geometry - * @return Coordinate at 'measure' distance along the geometry - * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 - * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html - */ - public static Coordinate locatePoint(Geometry geometry, double measure) { - return new LengthIndexedLine(geometry).extractPoint(measure); - } + /** + * Find the coordinate at the specified 'measure' distance along a + * Geometry. This is achieved through using the JTS + * LengthIndexedLine.extractPoint(measure) method for finding the + * coordinates at the specified measure along the geometry. It is equivalent + * to Oracle's SDO_LRS.LOCATE_PT. + * + * @param geometry Geometry to measure + * @param measure the distance along the geometry + * @return Coordinate at 'measure' distance along the geometry + * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 + * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html + */ + public static Coordinate locatePoint(Geometry geometry, double measure) { + return new LengthIndexedLine(geometry).extractPoint(measure); + } - /** - * Create a Point located at the specified 'measure' distance along a - * Geometry, and offset to the left of the Geometry by the specified offset - * distance. This is achieved through using the JTS - * LengthIndexedLine.extractPoint(measure) method for finding the - * coordinates at the specified measure along the geometry. It is equivalent - * to Oracle's SDO_LRS.LOCATE_PT. - * - * @param layer Layer the geometry is contained by, and is used to access the - * GeometryFactory for creating the Point - * @param geometry Geometry to measure - * @param measure the distance along the geometry - * @param offset the distance offset to the left (or right for negative numbers) - * @return Point at 'measure' distance along the geometry, and offset - * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 - * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html - */ - public static Point locatePoint(Layer layer, Geometry geometry, double measure, double offset) { - return layer.getGeometryFactory().createPoint(locatePoint(geometry, measure, offset)); - } + /** + * Create a Point located at the specified 'measure' distance along a + * Geometry, and offset to the left of the Geometry by the specified offset + * distance. This is achieved through using the JTS + * LengthIndexedLine.extractPoint(measure) method for finding the + * coordinates at the specified measure along the geometry. It is equivalent + * to Oracle's SDO_LRS.LOCATE_PT. + * + * @param layer Layer the geometry is contained by, and is used to access the + * GeometryFactory for creating the Point + * @param geometry Geometry to measure + * @param measure the distance along the geometry + * @param offset the distance offset to the left (or right for negative numbers) + * @return Point at 'measure' distance along the geometry, and offset + * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 + * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html + */ + public static Point locatePoint(Layer layer, Geometry geometry, double measure, double offset) { + return layer.getGeometryFactory().createPoint(locatePoint(geometry, measure, offset)); + } - /** - * Find the coordinate located at the specified 'measure' distance along a - * Geometry, and offset to the left of the Geometry by the specified offset - * distance. This is achieved through using the JTS - * LengthIndexedLine.extractPoint(measure) method for finding the - * coordinates at the specified measure along the geometry. It is equivalent - * to Oracle's SDO_LRS.LOCATE_PT. - * - * @param geometry Geometry to measure - * @param measure the distance along the geometry - * @param offset the distance offset to the left (or right for negative numbers) - * @return Point at 'measure' distance along the geometry, and offset - * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 - * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html - */ - public static Coordinate locatePoint(Geometry geometry, double measure, double offset) { - return new LengthIndexedLine(geometry).extractPoint(measure, offset); - } + /** + * Find the coordinate located at the specified 'measure' distance along a + * Geometry, and offset to the left of the Geometry by the specified offset + * distance. This is achieved through using the JTS + * LengthIndexedLine.extractPoint(measure) method for finding the + * coordinates at the specified measure along the geometry. It is equivalent + * to Oracle's SDO_LRS.LOCATE_PT. + * + * @param geometry Geometry to measure + * @param measure the distance along the geometry + * @param offset the distance offset to the left (or right for negative numbers) + * @return Point at 'measure' distance along the geometry, and offset + * @see http://download.oracle.com/docs/cd/B13789_01/appdev.101/b10826/sdo_lrs_ref.htm#i85478 + * @see http://www.vividsolutions.com/jts/javadoc/com/vividsolutions/jts/linearref/LengthIndexedLine.html + */ + public static Coordinate locatePoint(Geometry geometry, double measure, double offset) { + return new LengthIndexedLine(geometry).extractPoint(measure, offset); + } - /** - * Adjust the size and position of a ReferencedEnvelope using fractions of - * the current size. For example: - * - *

-     * bounds = adjustBounds(bounds, 0.3, new double[] { -0.1, 0.1 });
-     * 
- *

- * This will zoom in to show 30% of the height and width, and will also - * move the visible window 10% to the left and 10% up. - * - * @param bounds current envelope - * @param zoomFactor fraction of size to zoom in by - * @param offsetFactor fraction of size to offset visible window by - * @return adjusted envelope - */ - public static ReferencedEnvelope adjustBounds(ReferencedEnvelope bounds, - double zoomFactor, double[] offsetFactor) { - if (offsetFactor == null || offsetFactor.length < bounds.getDimension()) { - offsetFactor = new double[bounds.getDimension()]; - } - ReferencedEnvelope scaled = new ReferencedEnvelope(bounds); - if (Math.abs(zoomFactor - 1.0) > 0.01) { - double[] min = scaled.getLowerCorner().getCoordinate(); - double[] max = scaled.getUpperCorner().getCoordinate(); - for (int i = 0; i < scaled.getDimension(); i++) { - double span = scaled.getSpan(i); - double delta = (span - span * zoomFactor) / 2.0; - double shift = span * offsetFactor[i]; + /** + * Adjust the size and position of a ReferencedEnvelope using fractions of + * the current size. For example: + * + *

+	 * bounds = adjustBounds(bounds, 0.3, new double[] { -0.1, 0.1 });
+	 * 
+ *

+ * This will zoom in to show 30% of the height and width, and will also + * move the visible window 10% to the left and 10% up. + * + * @param bounds current envelope + * @param zoomFactor fraction of size to zoom in by + * @param offsetFactor fraction of size to offset visible window by + * @return adjusted envelope + */ + public static ReferencedEnvelope adjustBounds(ReferencedEnvelope bounds, + double zoomFactor, double[] offsetFactor) { + if (offsetFactor == null || offsetFactor.length < bounds.getDimension()) { + offsetFactor = new double[bounds.getDimension()]; + } + ReferencedEnvelope scaled = new ReferencedEnvelope(bounds); + if (Math.abs(zoomFactor - 1.0) > 0.01) { + double[] min = scaled.getLowerCorner().getCoordinate(); + double[] max = scaled.getUpperCorner().getCoordinate(); + for (int i = 0; i < scaled.getDimension(); i++) { + double span = scaled.getSpan(i); + double delta = (span - span * zoomFactor) / 2.0; + double shift = span * offsetFactor[i]; // System.out.println("Have offset["+i+"]: "+shift); - min[i] += shift + delta; - max[i] += shift - delta; - } - scaled = new ReferencedEnvelope(min[0], max[0], min[1], max[1], - scaled.getCoordinateReferenceSystem()); - } - return scaled; - } + min[i] += shift + delta; + max[i] += shift - delta; + } + scaled = new ReferencedEnvelope(min[0], max[0], min[1], max[1], + scaled.getCoordinateReferenceSystem()); + } + return scaled; + } - public static Envelope adjustBounds(Envelope bounds, double zoomFactor, double[] offset) { - if (offset == null || offset.length < 2) { - offset = new double[]{0, 0}; - } - Envelope scaled = new Envelope(bounds); - if (Math.abs(zoomFactor - 1.0) > 0.01) { - double[] min = new double[]{scaled.getMinX(), scaled.getMinY()}; - double[] max = new double[]{scaled.getMaxX(), scaled.getMaxY()}; - for (int i = 0; i < 2; i++) { - double shift = offset[i]; + public static Envelope adjustBounds(Envelope bounds, double zoomFactor, double[] offset) { + if (offset == null || offset.length < 2) { + offset = new double[]{0, 0}; + } + Envelope scaled = new Envelope(bounds); + if (Math.abs(zoomFactor - 1.0) > 0.01) { + double[] min = new double[]{scaled.getMinX(), scaled.getMinY()}; + double[] max = new double[]{scaled.getMaxX(), scaled.getMaxY()}; + for (int i = 0; i < 2; i++) { + double shift = offset[i]; // System.out.println("Have offset["+i+"]: "+shift); - double span = (i == 0) ? scaled.getWidth() : scaled.getHeight(); - double delta = (span - span * zoomFactor) / 2.0; - min[i] += shift + delta; - max[i] += shift - delta; - } - scaled = new Envelope(min[0], max[0], min[1], max[1]); - } - return scaled; - } + double span = (i == 0) ? scaled.getWidth() : scaled.getHeight(); + double delta = (span - span * zoomFactor) / 2.0; + min[i] += shift + delta; + max[i] += shift - delta; + } + scaled = new Envelope(min[0], max[0], min[1], max[1]); + } + return scaled; + } - /** - * Create an Envelope that should approximately include the specified number - * of geometries, based on a simple linear calculation of the geometry - * density. If the layer has fewer geometries, then the layer bounds will be - * returned. If the limit is set to zero (or negative), a point Envelope - * will be returned. - * - * @param layer the layer whose geometry density is to be used to estimate the - * size of the envelope - * @param point the coordinate around which to build the envelope - * @param limit the number of geometries to be included in the envelope - * @return an envelope designed to include the estimated number of - * geometries - */ - public static Envelope createEnvelopeForGeometryDensityEstimate(Transaction tx, Layer layer, Coordinate point, int limit) { - if (limit < 1) { - return new Envelope(point); - } - int count = layer.getIndex().count(tx); - if (count > limit) { - return createEnvelopeForGeometryDensityEstimate(tx, layer, point, (double) limit / (double) count); - } else { - return Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); - } - } + /** + * Create an Envelope that should approximately include the specified number + * of geometries, based on a simple linear calculation of the geometry + * density. If the layer has fewer geometries, then the layer bounds will be + * returned. If the limit is set to zero (or negative), a point Envelope + * will be returned. + * + * @param layer the layer whose geometry density is to be used to estimate the + * size of the envelope + * @param point the coordinate around which to build the envelope + * @param limit the number of geometries to be included in the envelope + * @return an envelope designed to include the estimated number of + * geometries + */ + public static Envelope createEnvelopeForGeometryDensityEstimate(Transaction tx, Layer layer, Coordinate point, + int limit) { + if (limit < 1) { + return new Envelope(point); + } + int count = layer.getIndex().count(tx); + if (count > limit) { + return createEnvelopeForGeometryDensityEstimate(tx, layer, point, (double) limit / (double) count); + } else { + return Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); + } + } - /** - * Create an Envelope that should approximately include the specified number - * of geometries, based on a simple linear calculation of the geometry - * density. If the layer has fewer geometries, then the layer bounds will be - * returned. If the limit is set to zero (or negative), a point Envelope - * will be returned. - *

* - * @param tx the Neo4j transaction to extract necessary data from the database - * @param layer the layer whose geometry density is to be used to estimate the size of the envelope - * @param point the coordinate around which to build the envelope - * @param fraction the fractional number of geometries to be included in the envelope - * @return an envelope designed to include the estimated number of geometries - */ - public static Envelope createEnvelopeForGeometryDensityEstimate(Transaction tx, Layer layer, Coordinate point, double fraction) { - if (fraction < 0.0) { - return new Envelope(point); - } - Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); - double width = bbox.getWidth() * fraction; - double height = bbox.getWidth() * fraction; - Envelope extent = new Envelope(point); - extent.expandToInclude(point.x - width / 2.0, point.y - height / 2.0); - extent.expandToInclude(point.x + width / 2.0, point.y + height / 2.0); - return extent; - } + /** + * Create an Envelope that should approximately include the specified number + * of geometries, based on a simple linear calculation of the geometry + * density. If the layer has fewer geometries, then the layer bounds will be + * returned. If the limit is set to zero (or negative), a point Envelope + * will be returned. + *

* + * + * @param tx the Neo4j transaction to extract necessary data from the database + * @param layer the layer whose geometry density is to be used to estimate the size of the envelope + * @param point the coordinate around which to build the envelope + * @param fraction the fractional number of geometries to be included in the envelope + * @return an envelope designed to include the estimated number of geometries + */ + public static Envelope createEnvelopeForGeometryDensityEstimate(Transaction tx, Layer layer, Coordinate point, + double fraction) { + if (fraction < 0.0) { + return new Envelope(point); + } + Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); + double width = bbox.getWidth() * fraction; + double height = bbox.getWidth() * fraction; + Envelope extent = new Envelope(point); + extent.expandToInclude(point.x - width / 2.0, point.y - height / 2.0); + extent.expandToInclude(point.x + width / 2.0, point.y + height / 2.0); + return extent; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/Utilities.java b/src/main/java/org/neo4j/gis/spatial/Utilities.java index c9ec95f3f..a2e5a4759 100644 --- a/src/main/java/org/neo4j/gis/spatial/Utilities.java +++ b/src/main/java/org/neo4j/gis/spatial/Utilities.java @@ -21,7 +21,8 @@ import java.util.Comparator; import java.util.Iterator; - +import org.geotools.api.filter.Filter; +import org.geotools.api.geometry.BoundingBox; import org.geotools.filter.AndImpl; import org.geotools.filter.GeometryFilterImpl; import org.geotools.filter.LiteralExpressionImpl; @@ -33,13 +34,10 @@ import org.geotools.filter.spatial.OverlapsImpl; import org.geotools.filter.spatial.TouchesImpl; import org.geotools.filter.spatial.WithinImpl; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.gis.spatial.rtree.EnvelopeDecoder; import org.neo4j.graphdb.Node; -import org.geotools.api.filter.Filter; - -import org.locationtech.jts.geom.Geometry; -import org.geotools.api.geometry.BoundingBox; public class Utilities { @@ -69,16 +67,17 @@ public static org.neo4j.gis.spatial.rtree.Envelope extractEnvelopeFromFilter(Fil } @SuppressWarnings("rawtypes") - private static org.neo4j.gis.spatial.rtree.Envelope extractEnvelopeFromFilter(Filter filter, boolean inspectAndFilters) { + private static org.neo4j.gis.spatial.rtree.Envelope extractEnvelopeFromFilter(Filter filter, + boolean inspectAndFilters) { if (filter instanceof BBOXImpl) { return extractEnvelopeFromBBox((BBOXImpl) filter); } else if (filter instanceof IntersectsImpl || - filter instanceof ContainsImpl || - filter instanceof CrossesImpl || - filter instanceof EqualsImpl || - filter instanceof OverlapsImpl || - filter instanceof TouchesImpl || - filter instanceof WithinImpl) { + filter instanceof ContainsImpl || + filter instanceof CrossesImpl || + filter instanceof EqualsImpl || + filter instanceof OverlapsImpl || + filter instanceof TouchesImpl || + filter instanceof WithinImpl) { return extractEnvelopeFromGeometryFilter((GeometryFilterImpl) filter); } else if (filter instanceof AndImpl && inspectAndFilters) { AndImpl andFilter = (AndImpl) filter; @@ -114,19 +113,20 @@ private static Envelope extractEnvelopeFromLiteralExpression(LiteralExpressionIm } } - private static Envelope extractEnvelopeFromBBox(BBOXImpl boundingBox) { + private static Envelope extractEnvelopeFromBBox(BBOXImpl boundingBox) { BoundingBox bbox = boundingBox.getBounds(); - return new Envelope(bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()); - } + return new Envelope(bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()); + } /** * Comparator for comparing nodes by compaing the xMin on their evelopes. */ public static class ComparatorOnXMin implements Comparator { + final private EnvelopeDecoder decoder; - public ComparatorOnXMin(EnvelopeDecoder decoder){ + public ComparatorOnXMin(EnvelopeDecoder decoder) { this.decoder = decoder; } @@ -140,9 +140,10 @@ public int compare(Node o1, Node o2) { * Comparator or comparing nodes by coparing the yMin on their envelopes. */ public static class ComparatorOnYMin implements Comparator { + final private EnvelopeDecoder decoder; - public ComparatorOnYMin(EnvelopeDecoder decoder){ + public ComparatorOnYMin(EnvelopeDecoder decoder) { this.decoder = decoder; } diff --git a/src/main/java/org/neo4j/gis/spatial/WKBGeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/WKBGeometryEncoder.java index 82e197a23..80477c811 100644 --- a/src/main/java/org/neo4j/gis/spatial/WKBGeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/WKBGeometryEncoder.java @@ -30,23 +30,23 @@ public class WKBGeometryEncoder extends AbstractSinglePropertyEncoder implements Configurable { - public Geometry decodeGeometry(Entity container) { - try { - WKBReader reader = new WKBReader(layer.getGeometryFactory()); - return reader.read((byte[]) container.getProperty(geomProperty)); - } catch (ParseException e) { - throw new SpatialDatabaseException(e.getMessage(), e); - } - } + public Geometry decodeGeometry(Entity container) { + try { + WKBReader reader = new WKBReader(layer.getGeometryFactory()); + return reader.read((byte[]) container.getProperty(geomProperty)); + } catch (ParseException e) { + throw new SpatialDatabaseException(e.getMessage(), e); + } + } - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - WKBWriter writer = new WKBWriter(); - container.setProperty(geomProperty, writer.write(geometry)); - } + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + WKBWriter writer = new WKBWriter(); + container.setProperty(geomProperty, writer.write(geometry)); + } - @Override - public String getSignature() { - return "WKB" + super.getSignature(); - } -} \ No newline at end of file + @Override + public String getSignature() { + return "WKB" + super.getSignature(); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/WKTGeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/WKTGeometryEncoder.java index 5195dbcd1..49ef11ab0 100644 --- a/src/main/java/org/neo4j/gis/spatial/WKTGeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/WKTGeometryEncoder.java @@ -30,23 +30,23 @@ public class WKTGeometryEncoder extends AbstractSinglePropertyEncoder implements Configurable { - public Geometry decodeGeometry(Entity container) { - try { - WKTReader reader = new WKTReader(layer.getGeometryFactory()); - return reader.read((String) container.getProperty(geomProperty)); - } catch (ParseException e) { - throw new SpatialDatabaseException(e.getMessage(), e); - } - } + public Geometry decodeGeometry(Entity container) { + try { + WKTReader reader = new WKTReader(layer.getGeometryFactory()); + return reader.read((String) container.getProperty(geomProperty)); + } catch (ParseException e) { + throw new SpatialDatabaseException(e.getMessage(), e); + } + } - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - WKTWriter writer = new WKTWriter(); - container.setProperty(geomProperty, writer.write(geometry)); - } + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + WKTWriter writer = new WKTWriter(); + container.setProperty(geomProperty, writer.write(geometry)); + } - @Override - public String getSignature() { - return "WKT" + super.getSignature(); - } -} \ No newline at end of file + @Override + public String getSignature() { + return "WKT" + super.getSignature(); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMapper.java b/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMapper.java index 755de4389..5635e9f21 100644 --- a/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMapper.java +++ b/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMapper.java @@ -20,11 +20,11 @@ package org.neo4j.gis.spatial.attributes; import java.util.HashMap; - import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; public abstract class PropertyMapper { + private String from; private String to; protected String type; @@ -64,7 +64,8 @@ public String from() { } public String key() { - return new StringBuffer().append(from).append("-").append(to).append("-").append(type).append("-").append(params).toString(); + return new StringBuffer().append(from).append("-").append(to).append("-").append(type).append("-") + .append(params).toString(); } public static PropertyMapper fromNode(Node node) { @@ -103,6 +104,7 @@ public Object map(Object value) { } private static class DeltaLongMapper extends PropertyMapper { + protected long reference; public DeltaLongMapper(String from, String to, String type, long reference) { @@ -134,13 +136,13 @@ public Object map(Object value) { private static class MapMapper extends PropertyMapper { - private HashMap map = new HashMap(); + private HashMap map = new HashMap(); public MapMapper(String from, String to, String type, String params) { super(from, to, type, params); - for(String param:params.split(",")){ + for (String param : params.split(",")) { String[] fields = param.split(":"); - map.put(fields[0],fields[1]); + map.put(fields[0], fields[1]); } } diff --git a/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMappingManager.java b/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMappingManager.java index a5b6eab01..c1780e709 100644 --- a/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMappingManager.java +++ b/src/main/java/org/neo4j/gis/spatial/attributes/PropertyMappingManager.java @@ -28,78 +28,81 @@ import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; -import org.neo4j.graphdb.ResourceIterable; import org.neo4j.graphdb.Transaction; public class PropertyMappingManager { - private final Layer layer; - private LinkedHashMap propertyMappers; - public PropertyMappingManager(Layer layer) { - this.layer = layer; - } + private final Layer layer; + private LinkedHashMap propertyMappers; - private LinkedHashMap getPropertyMappers(Transaction tx) { - if (propertyMappers == null) { - propertyMappers = new LinkedHashMap<>(); - for (PropertyMapper mapper : loadMappers(tx).values()) { - addPropertyMapper(tx, mapper); - } - } - return propertyMappers; - } + public PropertyMappingManager(Layer layer) { + this.layer = layer; + } - private Map loadMappers(Transaction tx) { - HashMap mappers = new HashMap<>(); - try (var relationships = layer.getLayerNode(tx).getRelationships(Direction.OUTGOING, SpatialRelationshipTypes.PROPERTY_MAPPING)) { - for (Relationship rel : relationships) { - Node node = rel.getEndNode(); - mappers.put(node, PropertyMapper.fromNode(node)); - } - } - return mappers; - } + private LinkedHashMap getPropertyMappers(Transaction tx) { + if (propertyMappers == null) { + propertyMappers = new LinkedHashMap<>(); + for (PropertyMapper mapper : loadMappers(tx).values()) { + addPropertyMapper(tx, mapper); + } + } + return propertyMappers; + } - private void save(Transaction tx) { - ArrayList toSave = new ArrayList<>(getPropertyMappers(tx).values()); - ArrayList toDelete = new ArrayList<>(); - for (Map.Entry entry : loadMappers(tx).entrySet()) { - if (!toSave.remove(entry.getValue())) { - toDelete.add(entry.getKey()); - } - } - for (Node node : toDelete) { - try(var relationships = node.getRelationships()){ - for (Relationship rel : relationships) { - rel.delete(); - } - } - node.delete(); - } - for (PropertyMapper mapper : toSave) { - Node node = tx.createNode(); - mapper.save(tx, node); - layer.getLayerNode(tx).createRelationshipTo(node, SpatialRelationshipTypes.PROPERTY_MAPPING); - } - } + private Map loadMappers(Transaction tx) { + HashMap mappers = new HashMap<>(); + try (var relationships = layer.getLayerNode(tx) + .getRelationships(Direction.OUTGOING, SpatialRelationshipTypes.PROPERTY_MAPPING)) { + for (Relationship rel : relationships) { + Node node = rel.getEndNode(); + mappers.put(node, PropertyMapper.fromNode(node)); + } + } + return mappers; + } - private void addPropertyMapper(Transaction tx, PropertyMapper mapper) { - getPropertyMappers(tx).put(mapper.to(), mapper); - save(tx); - } + private void save(Transaction tx) { + ArrayList toSave = new ArrayList<>(getPropertyMappers(tx).values()); + ArrayList toDelete = new ArrayList<>(); + for (Map.Entry entry : loadMappers(tx).entrySet()) { + if (!toSave.remove(entry.getValue())) { + toDelete.add(entry.getKey()); + } + } + for (Node node : toDelete) { + try (var relationships = node.getRelationships()) { + for (Relationship rel : relationships) { + rel.delete(); + } + } + node.delete(); + } + for (PropertyMapper mapper : toSave) { + Node node = tx.createNode(); + mapper.save(tx, node); + layer.getLayerNode(tx).createRelationshipTo(node, SpatialRelationshipTypes.PROPERTY_MAPPING); + } + } - private PropertyMapper removePropertyMapper(Transaction tx, String to) { - PropertyMapper mapper = getPropertyMappers(tx).remove(to); - if (mapper != null) save(tx); - return mapper; - } + private void addPropertyMapper(Transaction tx, PropertyMapper mapper) { + getPropertyMappers(tx).put(mapper.to(), mapper); + save(tx); + } - public PropertyMapper getPropertyMapper(Transaction tx, String to) { - return getPropertyMappers(tx).get(to); - } + private PropertyMapper removePropertyMapper(Transaction tx, String to) { + PropertyMapper mapper = getPropertyMappers(tx).remove(to); + if (mapper != null) { + save(tx); + } + return mapper; + } - public void addPropertyMapper(Transaction tx, String from, String to, String type, String params) { - addPropertyMapper(tx, PropertyMapper.fromParams(from, to, type, params)); - } + public PropertyMapper getPropertyMapper(Transaction tx, String to) { + return getPropertyMappers(tx).get(to); + } + + public void addPropertyMapper(Transaction tx, String from, String to, String type, String params) { + addPropertyMapper(tx, PropertyMapper.fromParams(from, to, type, params)); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/AbstractSinglePropertyEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/AbstractSinglePropertyEncoder.java index c1871a635..6c6cded67 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/AbstractSinglePropertyEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/AbstractSinglePropertyEncoder.java @@ -10,10 +10,12 @@ public abstract class AbstractSinglePropertyEncoder extends AbstractGeometryEnco public void setConfiguration(String configuration) { if (configuration != null && configuration.trim().length() > 0) { String[] fields = configuration.split(":"); - if (fields.length > 0) + if (fields.length > 0) { geomProperty = fields[0]; - if (fields.length > 1) + } + if (fields.length > 1) { bboxProperty = fields[1]; + } } } @@ -26,4 +28,4 @@ public String getConfiguration() { public String getSignature() { return "GeometryEncoder(geom='" + geomProperty + "', bbox='" + bboxProperty + "')"; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/Configurable.java b/src/main/java/org/neo4j/gis/spatial/encoders/Configurable.java index 63ed6f48a..232eff48c 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/Configurable.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/Configurable.java @@ -20,6 +20,8 @@ package org.neo4j.gis.spatial.encoders; public interface Configurable { + String getConfiguration(); + void setConfiguration(String configuration); } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java index 13fa58d09..5da93bef1 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/NativePointEncoder.java @@ -34,60 +34,73 @@ * Simple encoder that stores point geometries as one Neo4j Point property. */ public class NativePointEncoder extends AbstractGeometryEncoder implements Configurable { - private static final String DEFAULT_GEOM = "location"; - private static GeometryFactory geometryFactory; - private String locationProperty = DEFAULT_GEOM; - private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); - protected GeometryFactory getGeometryFactory() { - if (geometryFactory == null) geometryFactory = new GeometryFactory(); - return geometryFactory; - } + private static final String DEFAULT_GEOM = "location"; + private static GeometryFactory geometryFactory; + private String locationProperty = DEFAULT_GEOM; + private Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - int gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()); - if (gtype == GTYPE_POINT) { - container.setProperty("gtype", gtype); - Neo4jPoint neo4jPoint = new Neo4jPoint((Point) geometry, crs); - container.setProperty(locationProperty, neo4jPoint); - } else { - throw new IllegalArgumentException("Cannot store non-Point types as Native Neo4j properties: " + SpatialDatabaseService.convertGeometryTypeToName(gtype)); - } + protected GeometryFactory getGeometryFactory() { + if (geometryFactory == null) { + geometryFactory = new GeometryFactory(); + } + return geometryFactory; + } - } + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + int gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()); + if (gtype == GTYPE_POINT) { + container.setProperty("gtype", gtype); + Neo4jPoint neo4jPoint = new Neo4jPoint((Point) geometry, crs); + container.setProperty(locationProperty, neo4jPoint); + } else { + throw new IllegalArgumentException("Cannot store non-Point types as Native Neo4j properties: " + + SpatialDatabaseService.convertGeometryTypeToName(gtype)); + } - @Override - public Geometry decodeGeometry(Entity container) { - org.neo4j.graphdb.spatial.Point point = ((org.neo4j.graphdb.spatial.Point) container.getProperty(locationProperty)); - if (point.getCRS().getCode() != crs.getCode()) { - throw new IllegalStateException("Trying to decode geometry with wrong CRS: layer configured to crs=" + crs + ", but geometry has crs=" + point.getCRS().getCode()); - } - double[] coordinate = point.getCoordinate().getCoordinate(); - if (crs.dimensions() == 3) { - return getGeometryFactory().createPoint(new Coordinate(coordinate[0], coordinate[1], coordinate[2])); - } else { - return getGeometryFactory().createPoint(new Coordinate(coordinate[0], coordinate[1])); - } - } + } - @Override - public String getConfiguration() { - return locationProperty + ":" + bboxProperty + ": " + crs.getCode(); - } + @Override + public Geometry decodeGeometry(Entity container) { + org.neo4j.graphdb.spatial.Point point = ((org.neo4j.graphdb.spatial.Point) container.getProperty( + locationProperty)); + if (point.getCRS().getCode() != crs.getCode()) { + throw new IllegalStateException("Trying to decode geometry with wrong CRS: layer configured to crs=" + crs + + ", but geometry has crs=" + point.getCRS().getCode()); + } + double[] coordinate = point.getCoordinate().getCoordinate(); + if (crs.dimensions() == 3) { + return getGeometryFactory().createPoint(new Coordinate(coordinate[0], coordinate[1], coordinate[2])); + } else { + return getGeometryFactory().createPoint(new Coordinate(coordinate[0], coordinate[1])); + } + } - @Override - public void setConfiguration(String configuration) { - if (configuration != null && configuration.trim().length() > 0) { - String[] fields = configuration.split(":"); - if (fields.length > 0) locationProperty = fields[0]; - if (fields.length > 1) bboxProperty = fields[1]; - if (fields.length > 2) crs = Neo4jCRS.findCRS(fields[2]); - } - } + @Override + public String getConfiguration() { + return locationProperty + ":" + bboxProperty + ": " + crs.getCode(); + } - @Override - public String getSignature() { - return "NativePointEncoder(geometry='" + locationProperty + "', bbox='" + bboxProperty + "', crs=" + crs.getCode() + ")"; - } + @Override + public void setConfiguration(String configuration) { + if (configuration != null && configuration.trim().length() > 0) { + String[] fields = configuration.split(":"); + if (fields.length > 0) { + locationProperty = fields[0]; + } + if (fields.length > 1) { + bboxProperty = fields[1]; + } + if (fields.length > 2) { + crs = Neo4jCRS.findCRS(fields[2]); + } + } + } + + @Override + public String getSignature() { + return "NativePointEncoder(geometry='" + locationProperty + "', bbox='" + bboxProperty + "', crs=" + + crs.getCode() + ")"; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java index aa05032dc..e756b920d 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimpleGraphEncoder.java @@ -25,7 +25,11 @@ import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.AbstractGeometryEncoder; import org.neo4j.gis.spatial.SpatialDatabaseException; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Entity; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.traversal.Evaluators; import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; @@ -37,53 +41,57 @@ * @TODO: Consider generalizing this code and making a general linked list geometry store available in the library */ public class SimpleGraphEncoder extends AbstractGeometryEncoder { - private GeometryFactory geometryFactory; - protected enum SimpleRelationshipTypes implements RelationshipType { - FIRST, NEXT; - } + private GeometryFactory geometryFactory; - private GeometryFactory getGeometryFactory() { - if (geometryFactory == null) geometryFactory = new GeometryFactory(); - return geometryFactory; - } + protected enum SimpleRelationshipTypes implements RelationshipType { + FIRST, NEXT; + } - private Node testIsNode(Entity container) { - if (!(container instanceof Node)) { - throw new SpatialDatabaseException("Cannot decode non-node geometry: " + container); - } - return (Node) container; - } + private GeometryFactory getGeometryFactory() { + if (geometryFactory == null) { + geometryFactory = new GeometryFactory(); + } + return geometryFactory; + } - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - Node node = testIsNode(container); - node.setProperty("gtype", GTYPE_LINESTRING); - Node prev = null; - for (Coordinate coord : geometry.getCoordinates()) { - Node point = tx.createNode(); - point.setProperty("x", coord.x); - point.setProperty("y", coord.y); - point.setProperty("z", coord.z); - if (prev == null) { - node.createRelationshipTo(point, SimpleRelationshipTypes.FIRST); - } else { - prev.createRelationshipTo(point, SimpleRelationshipTypes.NEXT); - } - prev = point; - } - } + private Node testIsNode(Entity container) { + if (!(container instanceof Node)) { + throw new SpatialDatabaseException("Cannot decode non-node geometry: " + container); + } + return (Node) container; + } - public Geometry decodeGeometry(Entity container) { - Node node = testIsNode(container); - CoordinateList coordinates = new CoordinateList(); - TraversalDescription td = new MonoDirectionalTraversalDescription().depthFirst() - .relationships(SimpleRelationshipTypes.FIRST, Direction.OUTGOING) - .relationships(SimpleRelationshipTypes.NEXT, Direction.OUTGOING).breadthFirst() - .evaluator(Evaluators.excludeStartPosition()); - for (Node point : td.traverse(node).nodes()) { - coordinates.add(new Coordinate((Double) point.getProperty("x"), (Double) point.getProperty("y"), (Double) point.getProperty("z")), false); - } - return getGeometryFactory().createLineString(coordinates.toCoordinateArray()); - } -} \ No newline at end of file + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + Node node = testIsNode(container); + node.setProperty("gtype", GTYPE_LINESTRING); + Node prev = null; + for (Coordinate coord : geometry.getCoordinates()) { + Node point = tx.createNode(); + point.setProperty("x", coord.x); + point.setProperty("y", coord.y); + point.setProperty("z", coord.z); + if (prev == null) { + node.createRelationshipTo(point, SimpleRelationshipTypes.FIRST); + } else { + prev.createRelationshipTo(point, SimpleRelationshipTypes.NEXT); + } + prev = point; + } + } + + public Geometry decodeGeometry(Entity container) { + Node node = testIsNode(container); + CoordinateList coordinates = new CoordinateList(); + TraversalDescription td = new MonoDirectionalTraversalDescription().depthFirst() + .relationships(SimpleRelationshipTypes.FIRST, Direction.OUTGOING) + .relationships(SimpleRelationshipTypes.NEXT, Direction.OUTGOING).breadthFirst() + .evaluator(Evaluators.excludeStartPosition()); + for (Node point : td.traverse(node).nodes()) { + coordinates.add(new Coordinate((Double) point.getProperty("x"), (Double) point.getProperty("y"), + (Double) point.getProperty("z")), false); + } + return getGeometryFactory().createLineString(coordinates.toCoordinateArray()); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java index c3ae1ec6b..1d91fbff3 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePointEncoder.java @@ -31,52 +31,61 @@ * Simple encoder that stores point geometries as two x/y properties. */ public class SimplePointEncoder extends AbstractGeometryEncoder implements Configurable { - public static final String DEFAULT_X = "longitude"; - public static final String DEFAULT_Y = "latitude"; - protected GeometryFactory geometryFactory; - protected String xProperty = DEFAULT_X; - protected String yProperty = DEFAULT_Y; - protected GeometryFactory getGeometryFactory() { - if (geometryFactory == null) geometryFactory = new GeometryFactory(); - return geometryFactory; - } + public static final String DEFAULT_X = "longitude"; + public static final String DEFAULT_Y = "latitude"; + protected GeometryFactory geometryFactory; + protected String xProperty = DEFAULT_X; + protected String yProperty = DEFAULT_Y; - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - container.setProperty( - "gtype", - SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); - Coordinate[] coords = geometry.getCoordinates(); - container.setProperty(xProperty, coords[0].x); - container.setProperty(yProperty, coords[0].y); - } + protected GeometryFactory getGeometryFactory() { + if (geometryFactory == null) { + geometryFactory = new GeometryFactory(); + } + return geometryFactory; + } - @Override - public Geometry decodeGeometry(Entity container) { - double x = ((Number) container.getProperty(xProperty)).doubleValue(); - double y = ((Number) container.getProperty(yProperty)).doubleValue(); - Coordinate coordinate = new Coordinate(x, y); - return getGeometryFactory().createPoint(coordinate); - } + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + container.setProperty( + "gtype", + SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); + Coordinate[] coords = geometry.getCoordinates(); + container.setProperty(xProperty, coords[0].x); + container.setProperty(yProperty, coords[0].y); + } - @Override - public String getConfiguration() { - return xProperty + ":" + yProperty + ":" + bboxProperty; - } + @Override + public Geometry decodeGeometry(Entity container) { + double x = ((Number) container.getProperty(xProperty)).doubleValue(); + double y = ((Number) container.getProperty(yProperty)).doubleValue(); + Coordinate coordinate = new Coordinate(x, y); + return getGeometryFactory().createPoint(coordinate); + } - @Override - public void setConfiguration(String configuration) { - if (configuration != null && configuration.trim().length() > 0) { - String[] fields = configuration.split(":"); - if (fields.length > 0) xProperty = fields[0]; - if (fields.length > 1) yProperty = fields[1]; - if (fields.length > 2) bboxProperty = fields[2]; - } - } + @Override + public String getConfiguration() { + return xProperty + ":" + yProperty + ":" + bboxProperty; + } - @Override - public String getSignature() { - return "SimplePointEncoder(x='" + xProperty + "', y='" + yProperty + "', bbox='" + bboxProperty + "')"; - } + @Override + public void setConfiguration(String configuration) { + if (configuration != null && configuration.trim().length() > 0) { + String[] fields = configuration.split(":"); + if (fields.length > 0) { + xProperty = fields[0]; + } + if (fields.length > 1) { + yProperty = fields[1]; + } + if (fields.length > 2) { + bboxProperty = fields[2]; + } + } + } + + @Override + public String getSignature() { + return "SimplePointEncoder(x='" + xProperty + "', y='" + yProperty + "', bbox='" + bboxProperty + "')"; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java index 4c0a66f75..1354bed98 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/SimplePropertyEncoder.java @@ -35,33 +35,36 @@ * @TODO: Consider switching from Float to Double according to Davide Savazzi */ public class SimplePropertyEncoder extends AbstractGeometryEncoder { - protected GeometryFactory geometryFactory; - protected GeometryFactory getGeometryFactory() { - if (geometryFactory == null) geometryFactory = new GeometryFactory(); - return geometryFactory; - } + protected GeometryFactory geometryFactory; - @Override - protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { - container.setProperty("gtype", SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); - Coordinate[] coords = geometry.getCoordinates(); - float[] data = new float[coords.length * 2]; - for (int i = 0; i < coords.length; i++) { - data[i * 2 + 0] = (float) coords[i].x; - data[i * 2 + 1] = (float) coords[i].y; - } + protected GeometryFactory getGeometryFactory() { + if (geometryFactory == null) { + geometryFactory = new GeometryFactory(); + } + return geometryFactory; + } - container.setProperty("data", data); - } + @Override + protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { + container.setProperty("gtype", SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass())); + Coordinate[] coords = geometry.getCoordinates(); + float[] data = new float[coords.length * 2]; + for (int i = 0; i < coords.length; i++) { + data[i * 2 + 0] = (float) coords[i].x; + data[i * 2 + 1] = (float) coords[i].y; + } - @Override - public Geometry decodeGeometry(Entity container) { - float[] data = (float[]) container.getProperty("data"); - Coordinate[] coordinates = new Coordinate[data.length / 2]; - for (int i = 0; i < data.length / 2; i++) { - coordinates[i] = new Coordinate(data[2 * i + 0], data[2 * i + 1]); - } - return getGeometryFactory().createLineString(coordinates); - } -} \ No newline at end of file + container.setProperty("data", data); + } + + @Override + public Geometry decodeGeometry(Entity container) { + float[] data = (float[]) container.getProperty("data"); + Coordinate[] coordinates = new Coordinate[data.length / 2]; + for (int i = 0; i < data.length / 2; i++) { + coordinates[i] = new Coordinate(data[2 * i + 0], data[2 * i + 1]); + } + return getGeometryFactory().createLineString(coordinates); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jCRS.java b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jCRS.java index 28886f8a9..df388faa3 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jCRS.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jCRS.java @@ -22,44 +22,45 @@ import org.neo4j.values.storable.CoordinateReferenceSystem; public class Neo4jCRS implements org.neo4j.graphdb.spatial.CRS { - protected final CoordinateReferenceSystem crs; - public Neo4jCRS(CoordinateReferenceSystem crs) { - this.crs = crs; - } + protected final CoordinateReferenceSystem crs; - @Override - public int getCode() { - return crs.getCode(); - } + public Neo4jCRS(CoordinateReferenceSystem crs) { + this.crs = crs; + } - @Override - public String getType() { - return crs.getType(); - } + @Override + public int getCode() { + return crs.getCode(); + } - @Override - public String getHref() { - return crs.getHref(); - } + @Override + public String getType() { + return crs.getType(); + } - public int dimensions() { - return crs.getDimension(); - } + @Override + public String getHref() { + return crs.getHref(); + } - public static Neo4jCRS findCRS(String crs) { - switch (crs) { - case "WGS-84": // name in Neo4j CRS table - case "WGS84(DD)": // name in geotools crs library - return makeCRS(4326); - case "Cartesian": - return makeCRS(7203); - default: - throw new IllegalArgumentException("Cypher type system does not support CRS: " + crs); - } - } + public int dimensions() { + return crs.getDimension(); + } - public static Neo4jCRS makeCRS(final int code) { - return new Neo4jCRS(CoordinateReferenceSystem.get(code)); - } + public static Neo4jCRS findCRS(String crs) { + switch (crs) { + case "WGS-84": // name in Neo4j CRS table + case "WGS84(DD)": // name in geotools crs library + return makeCRS(4326); + case "Cartesian": + return makeCRS(7203); + default: + throw new IllegalArgumentException("Cypher type system does not support CRS: " + crs); + } + } + + public static Neo4jCRS makeCRS(final int code) { + return new Neo4jCRS(CoordinateReferenceSystem.get(code)); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jGeometry.java b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jGeometry.java index dd8805279..1e1272bb1 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jGeometry.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jGeometry.java @@ -19,41 +19,43 @@ */ package org.neo4j.gis.spatial.encoders.neo4j; -import org.neo4j.graphdb.spatial.CRS; -import org.neo4j.graphdb.spatial.Coordinate; - import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; +import org.neo4j.graphdb.spatial.CRS; +import org.neo4j.graphdb.spatial.Coordinate; public class Neo4jGeometry implements org.neo4j.graphdb.spatial.Geometry { - protected final String geometryType; - protected final CRS crs; - protected final List coordinates; - - public Neo4jGeometry(String geometryType, List coordinates, CRS crs) { - this.geometryType = geometryType; - this.coordinates = coordinates; - this.crs = crs; - } - - public String getGeometryType() { - return this.geometryType; - } - - public List getCoordinates() { - return this.coordinates; - } - - public CRS getCRS() { - return this.crs; - } - - public static String coordinateString(List coordinates) { - return coordinates.stream().map(c -> Arrays.stream(c.getCoordinate()).mapToObj(Double::toString).collect(Collectors.joining(", "))).collect(Collectors.joining(", ")); - } - - public String toString() { - return geometryType + "(" + coordinateString(coordinates) + ")[" + crs + "]"; - } + + protected final String geometryType; + protected final CRS crs; + protected final List coordinates; + + public Neo4jGeometry(String geometryType, List coordinates, CRS crs) { + this.geometryType = geometryType; + this.coordinates = coordinates; + this.crs = crs; + } + + public String getGeometryType() { + return this.geometryType; + } + + public List getCoordinates() { + return this.coordinates; + } + + public CRS getCRS() { + return this.crs; + } + + public static String coordinateString(List coordinates) { + return coordinates.stream() + .map(c -> Arrays.stream(c.getCoordinate()).mapToObj(Double::toString).collect(Collectors.joining(", "))) + .collect(Collectors.joining(", ")); + } + + public String toString() { + return geometryType + "(" + coordinateString(coordinates) + ")[" + crs + "]"; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jPoint.java b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jPoint.java index e550cd094..c83f973a6 100644 --- a/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jPoint.java +++ b/src/main/java/org/neo4j/gis/spatial/encoders/neo4j/Neo4jPoint.java @@ -1,28 +1,28 @@ package org.neo4j.gis.spatial.encoders.neo4j; +import java.util.ArrayList; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Point; -import java.util.ArrayList; - public class Neo4jPoint extends Neo4jGeometry implements org.neo4j.graphdb.spatial.Point { - private final org.neo4j.graphdb.spatial.Coordinate coordinate; - public Neo4jPoint(double[] coordinate, Neo4jCRS crs) { - super("Point", new ArrayList<>(), crs); - this.coordinate = new org.neo4j.graphdb.spatial.Coordinate(coordinate); - this.coordinates.add(this.coordinate); - } + private final org.neo4j.graphdb.spatial.Coordinate coordinate; + + public Neo4jPoint(double[] coordinate, Neo4jCRS crs) { + super("Point", new ArrayList<>(), crs); + this.coordinate = new org.neo4j.graphdb.spatial.Coordinate(coordinate); + this.coordinates.add(this.coordinate); + } - public Neo4jPoint(Coordinate coord, Neo4jCRS crs) { - super("Point", new ArrayList<>(), crs); - this.coordinate = (crs.dimensions() == 3) ? - new org.neo4j.graphdb.spatial.Coordinate(coord.x, coord.y, coord.z) : - new org.neo4j.graphdb.spatial.Coordinate(coord.x, coord.y); - this.coordinates.add(this.coordinate); - } + public Neo4jPoint(Coordinate coord, Neo4jCRS crs) { + super("Point", new ArrayList<>(), crs); + this.coordinate = (crs.dimensions() == 3) ? + new org.neo4j.graphdb.spatial.Coordinate(coord.x, coord.y, coord.z) : + new org.neo4j.graphdb.spatial.Coordinate(coord.x, coord.y); + this.coordinates.add(this.coordinate); + } - public Neo4jPoint(Point point, Neo4jCRS crs) { - this(point.getCoordinate(), crs); - } + public Neo4jPoint(Point point, Neo4jCRS crs) { + this(point.getCoordinate(), crs); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/filter/AbstractSearchIntersection.java b/src/main/java/org/neo4j/gis/spatial/filter/AbstractSearchIntersection.java index 236972be7..93b4e0a2e 100644 --- a/src/main/java/org/neo4j/gis/spatial/filter/AbstractSearchIntersection.java +++ b/src/main/java/org/neo4j/gis/spatial/filter/AbstractSearchIntersection.java @@ -19,18 +19,17 @@ */ package org.neo4j.gis.spatial.filter; -import org.neo4j.gis.spatial.rtree.filter.AbstractSearchEnvelopeIntersection; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.Layer; import org.neo4j.gis.spatial.Utilities; +import org.neo4j.gis.spatial.rtree.filter.AbstractSearchEnvelopeIntersection; import org.neo4j.graphdb.Node; -import org.locationtech.jts.geom.Geometry; - /** * @author Craig Taverner */ public abstract class AbstractSearchIntersection extends AbstractSearchEnvelopeIntersection { - + protected Geometry referenceGeometry; protected Layer layer; @@ -44,4 +43,4 @@ protected Geometry decode(Node geomNode) { return layer.getGeometryEncoder().decodeGeometry(geomNode); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/filter/SearchCQL.java b/src/main/java/org/neo4j/gis/spatial/filter/SearchCQL.java index 7a55ab00a..9e7307061 100644 --- a/src/main/java/org/neo4j/gis/spatial/filter/SearchCQL.java +++ b/src/main/java/org/neo4j/gis/spatial/filter/SearchCQL.java @@ -19,6 +19,7 @@ */ package org.neo4j.gis.spatial.filter; +import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.filter.text.cql2.CQLException; import org.geotools.filter.text.ecql.ECQL; @@ -30,7 +31,6 @@ import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; /** * Find geometries that have at least one point in common with the given @@ -38,41 +38,41 @@ */ public class SearchCQL implements SearchFilter { - private final Transaction tx; - private final Neo4jFeatureBuilder featureBuilder; - private final Layer layer; - private final org.geotools.api.filter.Filter filter; - private final Envelope filterEnvelope; + private final Transaction tx; + private final Neo4jFeatureBuilder featureBuilder; + private final Layer layer; + private final org.geotools.api.filter.Filter filter; + private final Envelope filterEnvelope; - public SearchCQL(Transaction tx, Layer layer, org.geotools.api.filter.Filter filter) { - this.tx = tx; - this.layer = layer; - this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); - this.filter = filter; - this.filterEnvelope = Utilities.extractEnvelopeFromFilter(filter); - } + public SearchCQL(Transaction tx, Layer layer, org.geotools.api.filter.Filter filter) { + this.tx = tx; + this.layer = layer; + this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); + this.filter = filter; + this.filterEnvelope = Utilities.extractEnvelopeFromFilter(filter); + } - public SearchCQL(Transaction tx, Layer layer, String cql) { - this.tx = tx; - this.layer = layer; - this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); - try { - this.filter = ECQL.toFilter(cql); - this.filterEnvelope = Utilities.extractEnvelopeFromFilter(filter); - } catch (CQLException e) { - throw new SpatialDatabaseException("CQLException: " + e.getMessage()); - } - } + public SearchCQL(Transaction tx, Layer layer, String cql) { + this.tx = tx; + this.layer = layer; + this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); + try { + this.filter = ECQL.toFilter(cql); + this.filterEnvelope = Utilities.extractEnvelopeFromFilter(filter); + } catch (CQLException e) { + throw new SpatialDatabaseException("CQLException: " + e.getMessage()); + } + } - @Override - public boolean needsToVisit(Envelope envelope) { - return filterEnvelope == null || filterEnvelope.intersects(envelope); - } + @Override + public boolean needsToVisit(Envelope envelope) { + return filterEnvelope == null || filterEnvelope.intersects(envelope); + } - @Override - public boolean geometryMatches(Transaction tx, Node geomNode) { - SimpleFeature feature = featureBuilder.buildFeature(tx, new SpatialDatabaseRecord(this.layer, geomNode)); - return filter.evaluate(feature); - } + @Override + public boolean geometryMatches(Transaction tx, Node geomNode) { + SimpleFeature feature = featureBuilder.buildFeature(tx, new SpatialDatabaseRecord(this.layer, geomNode)); + return filter.evaluate(feature); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersect.java b/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersect.java index 25cb05593..a687acd5f 100644 --- a/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersect.java +++ b/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersect.java @@ -19,16 +19,15 @@ */ package org.neo4j.gis.spatial.filter; -import org.neo4j.gis.spatial.rtree.Envelope; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.graphdb.Node; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that have at least one point in common with the given geometry - * + * * @author Davide Savazzi * @author Craig Taverner */ @@ -43,4 +42,4 @@ protected boolean onEnvelopeIntersection(Node geomNode, Envelope geomEnvelope) { return geometry.intersects(referenceGeometry); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersectWindow.java b/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersectWindow.java index bf9619137..9a15249f6 100644 --- a/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersectWindow.java +++ b/src/main/java/org/neo4j/gis/spatial/filter/SearchIntersectWindow.java @@ -19,17 +19,16 @@ */ package org.neo4j.gis.spatial.filter; -import org.neo4j.gis.spatial.index.Envelope; -import org.neo4j.gis.spatial.rtree.filter.AbstractSearchEnvelopeIntersection; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.Layer; import org.neo4j.gis.spatial.Utilities; +import org.neo4j.gis.spatial.index.Envelope; +import org.neo4j.gis.spatial.rtree.filter.AbstractSearchEnvelopeIntersection; import org.neo4j.graphdb.Node; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that intersect with the specified search window. - * + * * @author Craig Taverner */ public class SearchIntersectWindow extends AbstractSearchEnvelopeIntersection { @@ -37,9 +36,9 @@ public class SearchIntersectWindow extends AbstractSearchEnvelopeIntersection { private Layer layer; private Geometry windowGeom; - public SearchIntersectWindow(Layer layer, Envelope envelope) { - this(layer, Utilities.fromNeo4jToJts(envelope)); - } + public SearchIntersectWindow(Layer layer, Envelope envelope) { + this(layer, Utilities.fromNeo4jToJts(envelope)); + } public SearchIntersectWindow(Layer layer, org.locationtech.jts.geom.Envelope other) { super(layer.getGeometryEncoder(), Utilities.fromJtsToNeo4j(other)); @@ -56,4 +55,4 @@ protected boolean onEnvelopeIntersection(Node geomNode, org.neo4j.gis.spatial.rt return geometry.intersects(windowGeom); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/filter/SearchRecords.java b/src/main/java/org/neo4j/gis/spatial/filter/SearchRecords.java index 2d55450b6..ab8836216 100644 --- a/src/main/java/org/neo4j/gis/spatial/filter/SearchRecords.java +++ b/src/main/java/org/neo4j/gis/spatial/filter/SearchRecords.java @@ -19,47 +19,46 @@ */ package org.neo4j.gis.spatial.filter; +import java.util.Iterator; import org.neo4j.gis.spatial.Layer; import org.neo4j.gis.spatial.SpatialDatabaseRecord; import org.neo4j.gis.spatial.rtree.filter.SearchResults; import org.neo4j.graphdb.Node; -import java.util.Iterator; - public class SearchRecords implements Iterable, Iterator { - private final SearchResults results; - private final Iterator nodeIterator; - private final Layer layer; + private final SearchResults results; + private final Iterator nodeIterator; + private final Layer layer; - public SearchRecords(Layer layer, SearchResults results) { - this.layer = layer; - this.results = results; - nodeIterator = results.iterator(); - } + public SearchRecords(Layer layer, SearchResults results) { + this.layer = layer; + this.results = results; + nodeIterator = results.iterator(); + } - @Override - public Iterator iterator() { - return this; - } + @Override + public Iterator iterator() { + return this; + } - @Override - public boolean hasNext() { - return nodeIterator.hasNext(); - } + @Override + public boolean hasNext() { + return nodeIterator.hasNext(); + } - @Override - public SpatialDatabaseRecord next() { - return new SpatialDatabaseRecord(layer, nodeIterator.next()); - } + @Override + public SpatialDatabaseRecord next() { + return new SpatialDatabaseRecord(layer, nodeIterator.next()); + } - @Override - public void remove() { - throw new UnsupportedOperationException("Cannot remove from results"); - } + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove from results"); + } - public int count() { - return results.count(); - } + public int count() { + return results.count(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedMonitor.java b/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedMonitor.java index 19c62bb57..4fc14351e 100644 --- a/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedMonitor.java +++ b/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedMonitor.java @@ -20,22 +20,23 @@ package org.neo4j.gis.spatial.index; public class ExplicitIndexBackedMonitor { - long hits = 0L; - long misses = 0; - public void hit() { - hits++; - } + long hits = 0L; + long misses = 0; - public void miss() { - misses++; - } + public void hit() { + hits++; + } - public long getHits() { - return hits; - } + public void miss() { + misses++; + } - public long getMisses() { - return misses; - } + public long getHits() { + return hits; + } + + public long getMisses() { + return misses; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedPointIndex.java b/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedPointIndex.java index 58ba3c435..72bd3920d 100644 --- a/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedPointIndex.java +++ b/src/main/java/org/neo4j/gis/spatial/index/ExplicitIndexBackedPointIndex.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.stream.Stream; import org.neo4j.gis.spatial.Layer; import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.gis.spatial.rtree.Envelope; @@ -55,183 +54,185 @@ */ public abstract class ExplicitIndexBackedPointIndex implements LayerIndexReader, SpatialIndexWriter { - protected Layer layer; - private PropertyEncodingNodeIndex index; - private final ExplicitIndexBackedMonitor monitor = new ExplicitIndexBackedMonitor(); - - protected abstract String indexTypeName(); - - @Override - public void init(Transaction tx, IndexManager indexManager, Layer layer) { - this.layer = layer; - String indexName = "_SpatialIndex_" + indexTypeName() + "_" + layer.getName(); - Label label = Label.label("SpatialIndex_" + indexTypeName() + "_" + layer.getName()); - this.index = new PropertyEncodingNodeIndex<>(indexManager, indexName, label, indexName.toLowerCase()); - this.index.initialize(tx); - } - - @Override - public Layer getLayer() { - return layer; - } - - @Override - public SearchRecords search(Transaction tx, SearchFilter filter) { - return new SearchRecords(layer, searchIndex(tx, filter)); - } - - @Override - public void add(Transaction tx, Node geomNode) { - index.add(geomNode, getIndexValueFor(tx, geomNode)); - } - - protected abstract E getIndexValueFor(Transaction tx, Node geomNode); - - @Override - public void add(Transaction tx, List geomNodes) { - for (Node node : geomNodes) { - add(tx, node); - } - } - - @Override - public void remove(Transaction tx, String geomNodeId, boolean deleteGeomNode, boolean throwExceptionIfNotFound) { - try { - Node geomNode = tx.getNodeByElementId(geomNodeId); - if (geomNode != null) { - index.remove(geomNode); - if (deleteGeomNode) { - try (var relationships = geomNode.getRelationships()) { - for (Relationship rel : relationships) { - rel.delete(); - } - } - geomNode.delete(); - } - } - } catch (NotFoundException nfe) { - if (throwExceptionIfNotFound) { - throw nfe; - } - } - } - - @Override - public void removeAll(Transaction tx, boolean deleteGeomNodes, Listener monitor) { - if (deleteGeomNodes) { - for (Node node : getAllIndexedNodes(tx)) { - remove(tx, node.getElementId(), true, true); - } - } - index.delete(tx); - } - - @Override - public void clear(Transaction tx, Listener monitor) { - removeAll(tx, false, monitor); - } - - @Override - public EnvelopeDecoder getEnvelopeDecoder() { - return layer.getGeometryEncoder(); - } - - @Override - public boolean isEmpty(Transaction tx) { - return true; - } - - @Override - public int count(Transaction ignore) { - return 0; - } - - @Override - public Envelope getBoundingBox(Transaction tx) { - return null; - } - - @Override - public boolean isNodeIndexed(Transaction tx, String nodeId) { - return false; - } - - @Override - public Iterable getAllIndexedNodes(Transaction tx) { - return index.queryAll(tx); - } - - @Override - public SearchResults searchIndex(Transaction tx, SearchFilter filter) { - Iterator indexHits = index.query(tx, searcherFor(tx, filter)); - return new SearchResults(() -> new FilteredIndexIterator(tx, indexHits, filter)); - } - - private class FilteredIndexIterator implements Iterator { - private final Transaction tx; - private final Iterator inner; - private final SearchFilter filter; - private Node next = null; - - private FilteredIndexIterator(Transaction tx, Iterator inner, SearchFilter filter) { - this.tx = tx; - this.inner = inner; - this.filter = filter; - prefetch(); - } - - private void prefetch() { - next = null; - while (inner.hasNext()) { - Node node = inner.next(); - if (filter.geometryMatches(tx, node)) { - next = node; - monitor.hit(); - break; - } else { - monitor.miss(); - } - } - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public Node next() { - Node node = next; - if (node == null) { - throw new NoSuchElementException(); // GeoPipes relies on this behaviour instead of hasNext() - } else { - prefetch(); - return node; - } - } - } - - /** - * Create a class capable of performing a specific search based on a custom 2D to 1D conversion. - */ - protected abstract Neo4jIndexSearcher searcherFor(Transaction tx, SearchFilter filter); - - public interface Neo4jIndexSearcher { - Iterator search(KernelTransaction ktx, Label label, String propertyKey); - } - - @Override - public void addMonitor(TreeMonitor monitor) { - - } - - public ExplicitIndexBackedMonitor getMonitor() { - return this.monitor; - } - - @Override - public void configure(Map config) { - - } + protected Layer layer; + private PropertyEncodingNodeIndex index; + private final ExplicitIndexBackedMonitor monitor = new ExplicitIndexBackedMonitor(); + + protected abstract String indexTypeName(); + + @Override + public void init(Transaction tx, IndexManager indexManager, Layer layer) { + this.layer = layer; + String indexName = "_SpatialIndex_" + indexTypeName() + "_" + layer.getName(); + Label label = Label.label("SpatialIndex_" + indexTypeName() + "_" + layer.getName()); + this.index = new PropertyEncodingNodeIndex<>(indexManager, indexName, label, indexName.toLowerCase()); + this.index.initialize(tx); + } + + @Override + public Layer getLayer() { + return layer; + } + + @Override + public SearchRecords search(Transaction tx, SearchFilter filter) { + return new SearchRecords(layer, searchIndex(tx, filter)); + } + + @Override + public void add(Transaction tx, Node geomNode) { + index.add(geomNode, getIndexValueFor(tx, geomNode)); + } + + protected abstract E getIndexValueFor(Transaction tx, Node geomNode); + + @Override + public void add(Transaction tx, List geomNodes) { + for (Node node : geomNodes) { + add(tx, node); + } + } + + @Override + public void remove(Transaction tx, String geomNodeId, boolean deleteGeomNode, boolean throwExceptionIfNotFound) { + try { + Node geomNode = tx.getNodeByElementId(geomNodeId); + if (geomNode != null) { + index.remove(geomNode); + if (deleteGeomNode) { + try (var relationships = geomNode.getRelationships()) { + for (Relationship rel : relationships) { + rel.delete(); + } + } + geomNode.delete(); + } + } + } catch (NotFoundException nfe) { + if (throwExceptionIfNotFound) { + throw nfe; + } + } + } + + @Override + public void removeAll(Transaction tx, boolean deleteGeomNodes, Listener monitor) { + if (deleteGeomNodes) { + for (Node node : getAllIndexedNodes(tx)) { + remove(tx, node.getElementId(), true, true); + } + } + index.delete(tx); + } + + @Override + public void clear(Transaction tx, Listener monitor) { + removeAll(tx, false, monitor); + } + + @Override + public EnvelopeDecoder getEnvelopeDecoder() { + return layer.getGeometryEncoder(); + } + + @Override + public boolean isEmpty(Transaction tx) { + return true; + } + + @Override + public int count(Transaction ignore) { + return 0; + } + + @Override + public Envelope getBoundingBox(Transaction tx) { + return null; + } + + @Override + public boolean isNodeIndexed(Transaction tx, String nodeId) { + return false; + } + + @Override + public Iterable getAllIndexedNodes(Transaction tx) { + return index.queryAll(tx); + } + + @Override + public SearchResults searchIndex(Transaction tx, SearchFilter filter) { + Iterator indexHits = index.query(tx, searcherFor(tx, filter)); + return new SearchResults(() -> new FilteredIndexIterator(tx, indexHits, filter)); + } + + private class FilteredIndexIterator implements Iterator { + + private final Transaction tx; + private final Iterator inner; + private final SearchFilter filter; + private Node next = null; + + private FilteredIndexIterator(Transaction tx, Iterator inner, SearchFilter filter) { + this.tx = tx; + this.inner = inner; + this.filter = filter; + prefetch(); + } + + private void prefetch() { + next = null; + while (inner.hasNext()) { + Node node = inner.next(); + if (filter.geometryMatches(tx, node)) { + next = node; + monitor.hit(); + break; + } else { + monitor.miss(); + } + } + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Node next() { + Node node = next; + if (node == null) { + throw new NoSuchElementException(); // GeoPipes relies on this behaviour instead of hasNext() + } else { + prefetch(); + return node; + } + } + } + + /** + * Create a class capable of performing a specific search based on a custom 2D to 1D conversion. + */ + protected abstract Neo4jIndexSearcher searcherFor(Transaction tx, SearchFilter filter); + + public interface Neo4jIndexSearcher { + + Iterator search(KernelTransaction ktx, Label label, String propertyKey); + } + + @Override + public void addMonitor(TreeMonitor monitor) { + + } + + public ExplicitIndexBackedMonitor getMonitor() { + return this.monitor; + } + + @Override + public void configure(Map config) { + + } } diff --git a/src/main/java/org/neo4j/gis/spatial/index/IndexManager.java b/src/main/java/org/neo4j/gis/spatial/index/IndexManager.java index 1d567c621..b047eb994 100644 --- a/src/main/java/org/neo4j/gis/spatial/index/IndexManager.java +++ b/src/main/java/org/neo4j/gis/spatial/index/IndexManager.java @@ -1,5 +1,7 @@ package org.neo4j.gis.spatial.index; +import java.util.List; +import java.util.concurrent.TimeUnit; import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.schema.IndexDefinition; @@ -10,241 +12,246 @@ import org.neo4j.kernel.impl.api.security.RestrictedAccessMode; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.util.List; -import java.util.concurrent.TimeUnit; - public class IndexManager { - private final GraphDatabaseAPI db; - private final SecurityContext securityContext; - - public static class IndexAccessMode extends RestrictedAccessMode { - public static SecurityContext withIndexCreate(SecurityContext securityContext) { - return securityContext.withMode(new IndexAccessMode(securityContext)); - } - - private IndexAccessMode(SecurityContext securityContext) { - super(securityContext.mode(), Static.SCHEMA); - } - - @Override - public PermissionState allowsTokenCreates(PrivilegeAction action) { - return PermissionState.EXPLICIT_GRANT; - } - - @Override - public boolean allowsSchemaWrites() { - return true; - } - - @Override - public PermissionState allowsSchemaWrites(PrivilegeAction action) { - return PermissionState.EXPLICIT_GRANT; - } - } - - public IndexManager(GraphDatabaseAPI db, SecurityContext securityContext) { - this.db = db; - this.securityContext = IndexAccessMode.withIndexCreate(securityContext); - } - - /** - * Blocking call that spawns a thread to create an index and then waits for that thread to finish. - * This is highly likely to cause deadlocks on index checks, so be careful where it is used. - * Best used if you can commit any other outer transaction first, then run this, and after that - * start a new transaction. For example, see the OSMImport approaching to batching transactions. - * It is possible to use this in procedures with outer transactions if you can ensure the outer - * transactions are read-only. - */ - public IndexDefinition indexFor(Transaction tx, String indexName, Label label, String propertyKey) { - return indexFor(tx, indexName, label, propertyKey, true); - } - - /** - * Non-blocking call that spawns a thread to create an index and then waits for that thread to finish. - * Use this especially on indexes that are not immediately needed. Also use it if you have an outer - * transaction that cannot be committed before making this call. - */ - public void makeIndexFor(Transaction tx, String indexName, Label label, String propertyKey) { - indexFor(tx, indexName, label, propertyKey, false); - } - - private IndexDefinition indexFor(Transaction tx, String indexName, Label label, String propertyKey, boolean waitFor) { - for (IndexDefinition exists : tx.schema().getIndexes(label)) { - if (exists.getName().equals(indexName)) { - return exists; - } - } - String name = "IndexMaker(" + indexName + ")"; - Thread exists = findThread(name); - if (exists != null) { - throw new IllegalStateException("Already have thread: " + exists.getName()); - } else { - IndexMaker indexMaker = new IndexMaker(indexName, label, propertyKey); - Thread indexMakerThread = new Thread(indexMaker, name); - if (waitFor) { - indexMakerThread.start(); - try { - indexMakerThread.join(); - if (indexMaker.e != null) { - throw new RuntimeException("Failed to make index " + indexMaker.description(), indexMaker.e); - } - return indexMaker.index; - } catch (InterruptedException e) { - throw new RuntimeException("Failed to make index " + indexMaker.description(), e); - } - } else { - return null; - } - } - } - - public void deleteIndex(IndexDefinition index) { - String name = "IndexRemover(" + index.getName() + ")"; - Thread exists = findThread(name); - if (exists != null) { - System.out.println("Already have thread: " + exists.getName()); - } else { - IndexRemover indexRemover = new IndexRemover(index); - Thread indexRemoverThread = new Thread(indexRemover, name); - indexRemoverThread.start(); - } - } - - public void waitForDeletions() { - waitForThreads("IndexMaker"); - waitForThreads("IndexRemover"); - } - - private void waitForThreads(String prefix) { - Thread found; - while ((found = findThread(prefix)) != null) { - try { - found.join(); - } catch (InterruptedException e) { - throw new RuntimeException("Wait for thread " + found.getName(), e); - } - } - } - - private Thread findThread(String prefix) { - ThreadGroup rootGroup = Thread.currentThread().getThreadGroup(); - Thread found = findThread(rootGroup, prefix); - if (found != null) { - System.out.println("Found thread in current group[" + rootGroup.getName() + "}: " + prefix); - return found; - } - ThreadGroup parentGroup; - while ((parentGroup = rootGroup.getParent()) != null) { - rootGroup = parentGroup; - } - found = findThread(rootGroup, prefix); - if (found != null) { - System.out.println("Found thread in root group[" + rootGroup.getName() + "}: " + prefix); - return found; - } - return null; - } - - private Thread findThread(ThreadGroup group, String prefix) { - Thread[] threads = new Thread[group.activeCount()]; - while (group.enumerate(threads, true) == threads.length) { - threads = new Thread[threads.length * 2]; - } - for (Thread thread : threads) { - if (thread != null && thread.getName() != null && thread.getName().startsWith(prefix)) { - return thread; - } - } - return null; - } - - private class IndexMaker implements Runnable { - private final String indexName; - private final Label label; - private final String propertyKey; - private Exception e; - private IndexDefinition index; - - private IndexMaker(String indexName, Label label, String propertyKey) { - this.indexName = indexName; - this.label = label; - this.propertyKey = propertyKey; - this.e = null; - } - - @Override - public void run() { - try { - try (Transaction tx = db.beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext)) { - index = findIndex(tx); - if (index == null) { - index = tx.schema().indexFor(label).withName(indexName).on(propertyKey).create(); - } - tx.commit(); - } - try (Transaction tx = db.beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext)) { - tx.schema().awaitIndexOnline(indexName, 30, TimeUnit.SECONDS); - } - } catch (Exception e) { - this.e = e; - } - } - - private IndexDefinition findIndex(Transaction tx) { - for (IndexDefinition index : tx.schema().getIndexes()) { - if (indexMatches(index)) { - return index; - } - } - return null; - } - - private boolean indexMatches(IndexDefinition anIndex) { - if (anIndex.getName().equals(indexName)) { - try { - List

  * { "properties": {"type": "geometry"},
  *   "step": {"type": "GEOM", "direction": "INCOMING"
@@ -50,15 +54,15 @@
  *   }
  * }
  * 
- * + *

* This will work with OSM datasets, traversing from the geometry node to * the way node and then to the tags node to test if the way is a * residential street. */ public class DynamicIndexReader extends LayerIndexReaderWrapper { - + private JSONObject query; - + private class DynamicRecordCounter extends SpatialIndexRecordCounter { @Override @@ -91,23 +95,24 @@ private boolean queryIndexNode(Envelope indexNodeEnvelope) { * querying recursively each nodes properties on the way, as along as * the JSON contains to have properties to test, and traversal steps to * take. - * + * * @param geomNode * @return true if the node matches the query string, or the query - * string is empty + * string is empty */ private boolean queryLeafNode(Node geomNode) { // TODO: Extend support for more complex queries JSONObject properties = (JSONObject) query.get("properties"); JSONObject step = (JSONObject) query.get("step"); - return queryNodeProperties(geomNode,properties) && stepAndQuery(geomNode,step); + return queryNodeProperties(geomNode, properties) && stepAndQuery(geomNode, step); } - + private boolean stepAndQuery(Node source, JSONObject step) { if (step != null) { JSONObject properties = (JSONObject) step.get("properties"); RelationshipType relType = RelationshipType.withName(step.get("type").toString()); - Relationship rel = source.getSingleRelationship(relType, Direction.valueOf(step.get("direction").toString())); + Relationship rel = source.getSingleRelationship(relType, + Direction.valueOf(step.get("direction").toString())); if (rel != null) { Node node = rel.getOtherNode(source); step = (JSONObject) step.get("step"); @@ -126,17 +131,18 @@ private boolean queryNodeProperties(Node node, JSONObject properties) { System.out.println("Unexpected 'geometry' in query string"); properties.remove("geometry"); } - + for (Object key : properties.keySet()) { Object value = node.getProperty(key.toString(), null); Object match = properties.get(key); // TODO: Find a better way to solve minor type mismatches (Long!=Integer) than the string conversion below - if (value == null || (match != null && !value.equals(match) && !value.toString().equals(match.toString()))) { + if (value == null || (match != null && !value.equals(match) && !value.toString() + .equals(match.toString()))) { return false; } } } - + return true; } @@ -146,30 +152,30 @@ public int count(Transaction tx) { index.visit(tx, counter, index.getIndexRoot(tx)); return counter.getResult(); } - + private SearchFilter wrapSearchFilter(final SearchFilter filter) { return new SearchFilter() { @Override public boolean needsToVisit(Envelope envelope) { - return queryIndexNode(envelope) && - filter.needsToVisit(envelope); + return queryIndexNode(envelope) && + filter.needsToVisit(envelope); } @Override public boolean geometryMatches(Transaction tx, Node geomNode) { return queryLeafNode(geomNode) && filter.geometryMatches(tx, geomNode); - } + } }; - } - + } + @Override public SearchResults searchIndex(Transaction tx, final SearchFilter filter) { return index.searchIndex(tx, wrapSearchFilter(filter)); } - + @Override public SearchRecords search(Transaction tx, SearchFilter filter) { return index.search(tx, wrapSearchFilter(filter)); - } -} \ No newline at end of file + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/indexfilter/LayerIndexReaderWrapper.java b/src/main/java/org/neo4j/gis/spatial/indexfilter/LayerIndexReaderWrapper.java index 9d1a09e38..c8c36b43a 100644 --- a/src/main/java/org/neo4j/gis/spatial/indexfilter/LayerIndexReaderWrapper.java +++ b/src/main/java/org/neo4j/gis/spatial/indexfilter/LayerIndexReaderWrapper.java @@ -20,17 +20,16 @@ package org.neo4j.gis.spatial.indexfilter; import java.util.Map; - +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.gis.spatial.index.IndexManager; +import org.neo4j.gis.spatial.index.LayerIndexReader; +import org.neo4j.gis.spatial.index.LayerTreeIndexReader; import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.gis.spatial.rtree.EnvelopeDecoder; import org.neo4j.gis.spatial.rtree.TreeMonitor; import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.gis.spatial.rtree.filter.SearchResults; -import org.neo4j.gis.spatial.Layer; -import org.neo4j.gis.spatial.index.LayerIndexReader; -import org.neo4j.gis.spatial.index.LayerTreeIndexReader; -import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; @@ -53,7 +52,9 @@ public LayerIndexReaderWrapper(LayerTreeIndexReader index) { @Override public void init(Transaction tx, IndexManager indexManager, Layer layer) { - if (layer != getLayer()) throw new IllegalArgumentException("Cannot change layer associated with this index"); + if (layer != getLayer()) { + throw new IllegalArgumentException("Cannot change layer associated with this index"); + } } @Override @@ -72,7 +73,7 @@ public int count(Transaction tx) { } @Override - public boolean isNodeIndexed(Transaction tx, String nodeId) { + public boolean isNodeIndexed(Transaction tx, String nodeId) { return index.isNodeIndexed(tx, nodeId); } @@ -97,8 +98,7 @@ public SearchResults searchIndex(Transaction tx, SearchFilter filter) { } @Override - public void addMonitor( TreeMonitor monitor ) - { + public void addMonitor(TreeMonitor monitor) { } diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMDataset.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMDataset.java index c1ccf16fe..970d8d2ff 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMDataset.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMDataset.java @@ -19,10 +19,17 @@ */ package org.neo4j.gis.spatial.osm; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.GeometryEncoder; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SpatialDatabaseException; +import org.neo4j.gis.spatial.SpatialDataset; +import org.neo4j.gis.spatial.SpatialRelationshipTypes; import org.neo4j.gis.spatial.utilities.RelationshipTraversal; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Node; @@ -33,326 +40,330 @@ import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; -import java.util.Collections; -import java.util.Iterator; -import java.util.Objects; - public class OSMDataset implements SpatialDataset, Iterator { - private final OSMLayer layer; - private final String datasetNodeId; - private Iterator wayNodeIterator; - - public OSMDataset(OSMLayer layer, String datasetNodeId) { - this.layer = layer; - this.datasetNodeId = datasetNodeId; - this.layer.setDataset(this); - } - - /** - * This method is used to construct the dataset on an existing node when the node id is known, - * which is the case with OSM importers. - */ - public static OSMDataset withDatasetId(Transaction tx, OSMLayer layer, String datasetNodeId) { - Node datasetNode = tx.getNodeByElementId(datasetNodeId); - Node layerNode = layer.getLayerNode(tx); - Relationship rel = layerNode.getSingleRelationship(SpatialRelationshipTypes.LAYERS, Direction.INCOMING); - if (rel == null) { - datasetNode.createRelationshipTo(layerNode, SpatialRelationshipTypes.LAYERS); - } else { - Node node = rel.getStartNode(); - if (!node.equals(datasetNode)) { - throw new SpatialDatabaseException("Layer '" + layer + "' already belongs to another dataset: " + node); - } - } - return new OSMDataset(layer, datasetNodeId); - } - - /** - * This method is used to construct the dataset when only the layer node is known, and the - * dataset node needs to be searched for. - */ - public static OSMDataset fromLayer(Transaction tx, OSMLayer layer) { - Relationship rel = layer.getLayerNode(tx).getSingleRelationship(SpatialRelationshipTypes.LAYERS, Direction.INCOMING); - if (rel == null) { - throw new SpatialDatabaseException("Layer '" + layer + "' does not have an associated dataset"); - } else { - String datasetNodeId = rel.getStartNode().getElementId(); - return new OSMDataset(layer, datasetNodeId); - } - } - - public Iterable getAllUserNodes(Transaction tx) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.USERS, Direction.OUTGOING) - .relationships(OSMRelation.OSM_USER, Direction.OUTGOING) - .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.OSM_USER)); - return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); - } - - public Iterable getAllChangesetNodes(Transaction tx) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.USERS, Direction.OUTGOING) - .relationships(OSMRelation.OSM_USER, Direction.OUTGOING) - .relationships(OSMRelation.USER, Direction.INCOMING) - .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.USER)); - return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); - } - - public Iterable getAllWayNodes(Transaction tx) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.WAYS, Direction.OUTGOING) - .relationships(OSMRelation.NEXT, Direction.OUTGOING) - .evaluator(Evaluators.excludeStartPosition()); - return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); - } - - public Iterable getAllPointNodes(Transaction tx) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.WAYS, Direction.OUTGOING) - .relationships(OSMRelation.NEXT, Direction.OUTGOING) - .relationships(OSMRelation.FIRST_NODE, Direction.OUTGOING) - .relationships(OSMRelation.NODE, Direction.OUTGOING) - .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.NODE)); - return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); - } - - public Iterable getWayNodes(Node way) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.NEXT, Direction.OUTGOING) - .relationships(OSMRelation.NODE, Direction.OUTGOING) - .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.NODE)); - return td.traverse( - way.getSingleRelationship(OSMRelation.FIRST_NODE, Direction.OUTGOING).getEndNode() - ).nodes(); - } - - public Node getChangeset(Node way) { - try { - return way.getSingleRelationship(OSMRelation.CHANGESET, Direction.OUTGOING).getEndNode(); - } catch (Exception e) { - System.out.println("Node has no changeset: " + e.getMessage()); - return null; - } - } - - public Node getUser(Node nodeWayOrChangeset) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.CHANGESET, Direction.OUTGOING) - .relationships(OSMRelation.USER, Direction.OUTGOING) - .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.USER)); - return RelationshipTraversal.getFirstNode(td.traverse(nodeWayOrChangeset).nodes()); - } - - public Way getWayFromId(Transaction tx, String id) { - return getWayFrom(tx.getNodeByElementId(id)); - } - - public Way getWayFrom(Node osmNodeOrWayNodeOrGeomNode) { - TraversalDescription td = new MonoDirectionalTraversalDescription() - .depthFirst() - .relationships(OSMRelation.NODE, Direction.INCOMING) - .relationships(OSMRelation.NEXT, Direction.INCOMING) - .relationships(OSMRelation.FIRST_NODE, Direction.INCOMING) - .relationships(OSMRelation.GEOM, Direction.INCOMING) - .evaluator(path -> path.endNode().hasProperty("way_osm_id") ? Evaluation.INCLUDE_AND_PRUNE - : Evaluation.EXCLUDE_AND_CONTINUE); - Node node = RelationshipTraversal.getFirstNode(td.traverse(osmNodeOrWayNodeOrGeomNode).nodes()); - return node != null ? new Way(node) : null; - } - - public class OSMNode { - protected Node node; - protected Node geomNode; - protected Geometry geometry; - - OSMNode(Node node) { - this.node = node; - Relationship geomRel = this.node.getSingleRelationship(OSMRelation.GEOM, Direction.OUTGOING); - if (geomRel != null) geomNode = geomRel.getEndNode(); - } - - public Way getWay() { - return OSMDataset.this.getWayFrom(this.node); - } - - public Geometry getGeometry() { - if (geometry == null && geomNode != null) { - geometry = layer.getGeometryEncoder().decodeGeometry(geomNode); - } - return geometry; - } - - public Envelope getEnvelope() { - return getGeometry().getEnvelopeInternal(); - } - - public boolean equals(OSMNode other) { - return Objects.equals(this.node.getElementId(), other.node.getElementId()); - } - - public Node getNode() { - return node; - } - - public String toString() { - if (node.hasProperty("name")) { - return node.getProperty("name").toString(); - } else if (getGeometry() != null) { - return getGeometry().getGeometryType(); - } else { - return node.toString(); - } - } - } - - public class Way extends OSMNode implements Iterable, Iterator { - private Iterator wayPointNodeIterator; - - Way(Node node) { - super(node); - } - - Iterable getWayNodes() { - return OSMDataset.this.getWayNodes(this.node); - } - - public Iterable getWayPoints() { - return this; - } - - public Iterator iterator() { - if (wayPointNodeIterator == null || !wayPointNodeIterator.hasNext()) { - wayPointNodeIterator = getWayNodes().iterator(); - } - return this; - } - - public boolean hasNext() { - return wayPointNodeIterator.hasNext(); - } - - public WayPoint next() { - return new WayPoint(wayPointNodeIterator.next()); - } - - public void remove() { - throw new UnsupportedOperationException("Cannot modify way-point collection"); - } - - public WayPoint getPointAt(Coordinate coordinate) { - for (WayPoint wayPoint : getWayPoints()) { - if (wayPoint.isAt(coordinate)) - return wayPoint; - } - return null; - } - - } - - public class WayPoint extends OSMNode { - WayPoint(Node node) { - super(node); - } - - boolean isAt(Coordinate coord) { - return getCoordinate().equals(coord); - } - - public Coordinate getCoordinate() { - return new Coordinate(getX(), getY()); - } - - private double getY() { - return (Double) node.getProperty("latitude", 0.0); - } - - private double getX() { - return (Double) node.getProperty("longitude", 0.0); - } - } - - @Override - public Iterable getAllGeometries(Transaction tx) { - //@TODO: support multiple layers - return layer.getAllGeometries(tx); - } - - @Override - public Iterable getAllGeometryNodes(Transaction tx) { - //@TODO: support multiple layers - return layer.getAllGeometryNodes(tx); - } - - @Override - public boolean containsGeometryNode(Transaction tx, Node geomNode) { - //@TODO: support multiple layers - return layer.containsGeometryNode(tx, geomNode); - } - - @Override - public GeometryEncoder getGeometryEncoder() { - //@TODO: support multiple layers - return layer.getGeometryEncoder(); - } - - @Override - public Iterable getLayers() { - return Collections.singletonList(layer); - } - - public Iterable getWays(final Transaction tx) { - return () -> OSMDataset.this.iterator(tx); - } - - public Iterator iterator(Transaction tx) { - if (wayNodeIterator == null || !wayNodeIterator.hasNext()) { - wayNodeIterator = getAllWayNodes(tx).iterator(); - } - return this; - } - - @Override - public boolean hasNext() { - return wayNodeIterator.hasNext(); - } - - @Override - public Way next() { - return new Way(wayNodeIterator.next()); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Cannot modify way collection"); - } - - public int getPoiCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("poiCount", 0); - } - - public int getNodeCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("nodeCount", 0); - } - - public int getWayCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("wayCount", 0); - } - - public int getRelationCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("relationCount", 0); - } - - public int getChangesetCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("changesetCount", 0); - } - - public int getUserCount(Transaction tx) { - return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("userCount", 0); - } + + private final OSMLayer layer; + private final String datasetNodeId; + private Iterator wayNodeIterator; + + public OSMDataset(OSMLayer layer, String datasetNodeId) { + this.layer = layer; + this.datasetNodeId = datasetNodeId; + this.layer.setDataset(this); + } + + /** + * This method is used to construct the dataset on an existing node when the node id is known, + * which is the case with OSM importers. + */ + public static OSMDataset withDatasetId(Transaction tx, OSMLayer layer, String datasetNodeId) { + Node datasetNode = tx.getNodeByElementId(datasetNodeId); + Node layerNode = layer.getLayerNode(tx); + Relationship rel = layerNode.getSingleRelationship(SpatialRelationshipTypes.LAYERS, Direction.INCOMING); + if (rel == null) { + datasetNode.createRelationshipTo(layerNode, SpatialRelationshipTypes.LAYERS); + } else { + Node node = rel.getStartNode(); + if (!node.equals(datasetNode)) { + throw new SpatialDatabaseException("Layer '" + layer + "' already belongs to another dataset: " + node); + } + } + return new OSMDataset(layer, datasetNodeId); + } + + /** + * This method is used to construct the dataset when only the layer node is known, and the + * dataset node needs to be searched for. + */ + public static OSMDataset fromLayer(Transaction tx, OSMLayer layer) { + Relationship rel = layer.getLayerNode(tx) + .getSingleRelationship(SpatialRelationshipTypes.LAYERS, Direction.INCOMING); + if (rel == null) { + throw new SpatialDatabaseException("Layer '" + layer + "' does not have an associated dataset"); + } else { + String datasetNodeId = rel.getStartNode().getElementId(); + return new OSMDataset(layer, datasetNodeId); + } + } + + public Iterable getAllUserNodes(Transaction tx) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.USERS, Direction.OUTGOING) + .relationships(OSMRelation.OSM_USER, Direction.OUTGOING) + .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.OSM_USER)); + return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); + } + + public Iterable getAllChangesetNodes(Transaction tx) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.USERS, Direction.OUTGOING) + .relationships(OSMRelation.OSM_USER, Direction.OUTGOING) + .relationships(OSMRelation.USER, Direction.INCOMING) + .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.USER)); + return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); + } + + public Iterable getAllWayNodes(Transaction tx) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.WAYS, Direction.OUTGOING) + .relationships(OSMRelation.NEXT, Direction.OUTGOING) + .evaluator(Evaluators.excludeStartPosition()); + return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); + } + + public Iterable getAllPointNodes(Transaction tx) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.WAYS, Direction.OUTGOING) + .relationships(OSMRelation.NEXT, Direction.OUTGOING) + .relationships(OSMRelation.FIRST_NODE, Direction.OUTGOING) + .relationships(OSMRelation.NODE, Direction.OUTGOING) + .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.NODE)); + return td.traverse(tx.getNodeByElementId(datasetNodeId)).nodes(); + } + + public Iterable getWayNodes(Node way) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.NEXT, Direction.OUTGOING) + .relationships(OSMRelation.NODE, Direction.OUTGOING) + .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.NODE)); + return td.traverse( + way.getSingleRelationship(OSMRelation.FIRST_NODE, Direction.OUTGOING).getEndNode() + ).nodes(); + } + + public Node getChangeset(Node way) { + try { + return way.getSingleRelationship(OSMRelation.CHANGESET, Direction.OUTGOING).getEndNode(); + } catch (Exception e) { + System.out.println("Node has no changeset: " + e.getMessage()); + return null; + } + } + + public Node getUser(Node nodeWayOrChangeset) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.CHANGESET, Direction.OUTGOING) + .relationships(OSMRelation.USER, Direction.OUTGOING) + .evaluator(Evaluators.includeWhereLastRelationshipTypeIs(OSMRelation.USER)); + return RelationshipTraversal.getFirstNode(td.traverse(nodeWayOrChangeset).nodes()); + } + + public Way getWayFromId(Transaction tx, String id) { + return getWayFrom(tx.getNodeByElementId(id)); + } + + public Way getWayFrom(Node osmNodeOrWayNodeOrGeomNode) { + TraversalDescription td = new MonoDirectionalTraversalDescription() + .depthFirst() + .relationships(OSMRelation.NODE, Direction.INCOMING) + .relationships(OSMRelation.NEXT, Direction.INCOMING) + .relationships(OSMRelation.FIRST_NODE, Direction.INCOMING) + .relationships(OSMRelation.GEOM, Direction.INCOMING) + .evaluator(path -> path.endNode().hasProperty("way_osm_id") ? Evaluation.INCLUDE_AND_PRUNE + : Evaluation.EXCLUDE_AND_CONTINUE); + Node node = RelationshipTraversal.getFirstNode(td.traverse(osmNodeOrWayNodeOrGeomNode).nodes()); + return node != null ? new Way(node) : null; + } + + public class OSMNode { + + protected Node node; + protected Node geomNode; + protected Geometry geometry; + + OSMNode(Node node) { + this.node = node; + Relationship geomRel = this.node.getSingleRelationship(OSMRelation.GEOM, Direction.OUTGOING); + if (geomRel != null) { + geomNode = geomRel.getEndNode(); + } + } + + public Way getWay() { + return OSMDataset.this.getWayFrom(this.node); + } + + public Geometry getGeometry() { + if (geometry == null && geomNode != null) { + geometry = layer.getGeometryEncoder().decodeGeometry(geomNode); + } + return geometry; + } + + public Envelope getEnvelope() { + return getGeometry().getEnvelopeInternal(); + } + + public boolean equals(OSMNode other) { + return Objects.equals(this.node.getElementId(), other.node.getElementId()); + } + + public Node getNode() { + return node; + } + + public String toString() { + if (node.hasProperty("name")) { + return node.getProperty("name").toString(); + } else if (getGeometry() != null) { + return getGeometry().getGeometryType(); + } else { + return node.toString(); + } + } + } + + public class Way extends OSMNode implements Iterable, Iterator { + + private Iterator wayPointNodeIterator; + + Way(Node node) { + super(node); + } + + Iterable getWayNodes() { + return OSMDataset.this.getWayNodes(this.node); + } + + public Iterable getWayPoints() { + return this; + } + + public Iterator iterator() { + if (wayPointNodeIterator == null || !wayPointNodeIterator.hasNext()) { + wayPointNodeIterator = getWayNodes().iterator(); + } + return this; + } + + public boolean hasNext() { + return wayPointNodeIterator.hasNext(); + } + + public WayPoint next() { + return new WayPoint(wayPointNodeIterator.next()); + } + + public void remove() { + throw new UnsupportedOperationException("Cannot modify way-point collection"); + } + + public WayPoint getPointAt(Coordinate coordinate) { + for (WayPoint wayPoint : getWayPoints()) { + if (wayPoint.isAt(coordinate)) { + return wayPoint; + } + } + return null; + } + + } + + public class WayPoint extends OSMNode { + + WayPoint(Node node) { + super(node); + } + + boolean isAt(Coordinate coord) { + return getCoordinate().equals(coord); + } + + public Coordinate getCoordinate() { + return new Coordinate(getX(), getY()); + } + + private double getY() { + return (Double) node.getProperty("latitude", 0.0); + } + + private double getX() { + return (Double) node.getProperty("longitude", 0.0); + } + } + + @Override + public Iterable getAllGeometries(Transaction tx) { + //@TODO: support multiple layers + return layer.getAllGeometries(tx); + } + + @Override + public Iterable getAllGeometryNodes(Transaction tx) { + //@TODO: support multiple layers + return layer.getAllGeometryNodes(tx); + } + + @Override + public boolean containsGeometryNode(Transaction tx, Node geomNode) { + //@TODO: support multiple layers + return layer.containsGeometryNode(tx, geomNode); + } + + @Override + public GeometryEncoder getGeometryEncoder() { + //@TODO: support multiple layers + return layer.getGeometryEncoder(); + } + + @Override + public Iterable getLayers() { + return Collections.singletonList(layer); + } + + public Iterable getWays(final Transaction tx) { + return () -> OSMDataset.this.iterator(tx); + } + + public Iterator iterator(Transaction tx) { + if (wayNodeIterator == null || !wayNodeIterator.hasNext()) { + wayNodeIterator = getAllWayNodes(tx).iterator(); + } + return this; + } + + @Override + public boolean hasNext() { + return wayNodeIterator.hasNext(); + } + + @Override + public Way next() { + return new Way(wayNodeIterator.next()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot modify way collection"); + } + + public int getPoiCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("poiCount", 0); + } + + public int getNodeCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("nodeCount", 0); + } + + public int getWayCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("wayCount", 0); + } + + public int getRelationCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("relationCount", 0); + } + + public int getChangesetCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("changesetCount", 0); + } + + public int getUserCount(Transaction tx) { + return (Integer) tx.getNodeByElementId(this.datasetNodeId).getProperty("userCount", 0); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMGeometryEncoder.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMGeometryEncoder.java index b98a45514..fa11f45c8 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMGeometryEncoder.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMGeometryEncoder.java @@ -27,14 +27,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; - -import org.neo4j.gis.spatial.rtree.Envelope; -import org.neo4j.gis.spatial.AbstractGeometryEncoder; -import org.neo4j.gis.spatial.SpatialDatabaseException; -import org.neo4j.gis.spatial.SpatialDatabaseService; -import org.neo4j.graphdb.*; -import org.neo4j.graphdb.traversal.TraversalDescription; - import org.locationtech.jts.algorithm.ConvexHull; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; @@ -42,6 +34,17 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Polygon; +import org.neo4j.gis.spatial.AbstractGeometryEncoder; +import org.neo4j.gis.spatial.SpatialDatabaseException; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.rtree.Envelope; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Entity; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; public class OSMGeometryEncoder extends AbstractGeometryEncoder { @@ -60,6 +63,7 @@ public class OSMGeometryEncoder extends AbstractGeometryEncoder { * no properties on a geometry. */ private static final class NullProperties implements Entity { + @Override public Object getProperty(String key) { return null; @@ -104,17 +108,18 @@ public Object removeProperty(String key) { public void setProperty(String key, Object value) { } - @Override - public String getElementId() { - return null; - } + @Override + public String getElementId() { + return null; + } - @Override - public void delete() { - } + @Override + public void delete() { + } } public static class OSMGraphException extends SpatialDatabaseException { + private static final long serialVersionUID = -6892234738075001044L; OSMGraphException(String message) { @@ -143,7 +148,7 @@ public Envelope decodeEnvelope(Entity container) { @Override public void encodeEnvelope(Envelope mbb, Entity container) { - container.setProperty(PROP_BBOX, new double[] { mbb.getMinX(), mbb.getMaxX(), mbb.getMinY(), mbb.getMaxY() }); + container.setProperty(PROP_BBOX, new double[]{mbb.getMinX(), mbb.getMaxX(), mbb.getMinY(), mbb.getMaxY()}); } public static Node getOSMNodeFromGeometryNode(Node geomNode) { @@ -160,10 +165,12 @@ public static Node getGeometryNodeFromOSMNode(Node osmNode) { * the proxy nodes. */ private static class NodeProxyIterator implements Iterator { + Iterator traverser; NodeProxyIterator(Node first) { - TraversalDescription traversalDescription = new MonoDirectionalTraversalDescription().relationships(OSMRelation.NEXT, Direction.OUTGOING); + TraversalDescription traversalDescription = new MonoDirectionalTraversalDescription() + .relationships(OSMRelation.NEXT, Direction.OUTGOING); traverser = createTraverserInBackwardsCompatibleWay(traversalDescription, first).iterator(); } @@ -214,54 +221,55 @@ public Geometry decodeGeometry(Entity container) { private Geometry decodeGeometryFromRelation(Node osmNode, int gtype, GeometryFactory geomFactory) { switch (gtype) { - case GTYPE_POLYGON: - LinearRing outer = null; - ArrayList inner = new ArrayList(); - // ArrayList rings = new ArrayList(); - try(var relationships = osmNode.getRelationships(Direction.OUTGOING, OSMRelation.MEMBER)) { - for (Relationship rel : relationships) { - Node wayNode = rel.getEndNode(); - String role = (String) rel.getProperty("role", null); - if (role != null) { - LinearRing ring = getOuterLinearRingFromGeometry(decodeGeometryFromWay(wayNode, GTYPE_POLYGON, -1, geomFactory)); - if (role.equals("outer")) { - outer = ring; - } else if (role.equals("inner")) { - inner.add(ring); - } - } - } - } - if (outer != null) { - return geomFactory.createPolygon(outer, inner.toArray(new LinearRing[inner.size()])); - } else { - return null; - } - case GTYPE_MULTIPOLYGON: - ArrayList polygons = new ArrayList<>(); - try(var relationships = osmNode.getRelationships(Direction.OUTGOING, OSMRelation.MEMBER)){ - for (Relationship rel : relationships) { - Node member = rel.getEndNode(); - Geometry geometry = null; - if (member.hasProperty("way_osm_id")) { - // decode simple polygons from ways - geometry = decodeGeometryFromWay(member, GTYPE_POLYGON, -1, geomFactory); - } else if (!member.hasProperty("node_osm_id")) { - // decode polygons with holes from relations - geometry = decodeGeometryFromRelation(member, GTYPE_POLYGON, geomFactory); + case GTYPE_POLYGON: + LinearRing outer = null; + ArrayList inner = new ArrayList(); + // ArrayList rings = new ArrayList(); + try (var relationships = osmNode.getRelationships(Direction.OUTGOING, OSMRelation.MEMBER)) { + for (Relationship rel : relationships) { + Node wayNode = rel.getEndNode(); + String role = (String) rel.getProperty("role", null); + if (role != null) { + LinearRing ring = getOuterLinearRingFromGeometry( + decodeGeometryFromWay(wayNode, GTYPE_POLYGON, -1, geomFactory)); + if (role.equals("outer")) { + outer = ring; + } else if (role.equals("inner")) { + inner.add(ring); + } + } + } } - if (geometry != null && geometry instanceof Polygon) { - polygons.add((Polygon) geometry); + if (outer != null) { + return geomFactory.createPolygon(outer, inner.toArray(new LinearRing[inner.size()])); + } else { + return null; } - } - } - if (polygons.size() > 0) { - return geomFactory.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()])); - } else { + case GTYPE_MULTIPOLYGON: + ArrayList polygons = new ArrayList<>(); + try (var relationships = osmNode.getRelationships(Direction.OUTGOING, OSMRelation.MEMBER)) { + for (Relationship rel : relationships) { + Node member = rel.getEndNode(); + Geometry geometry = null; + if (member.hasProperty("way_osm_id")) { + // decode simple polygons from ways + geometry = decodeGeometryFromWay(member, GTYPE_POLYGON, -1, geomFactory); + } else if (!member.hasProperty("node_osm_id")) { + // decode polygons with holes from relations + geometry = decodeGeometryFromRelation(member, GTYPE_POLYGON, geomFactory); + } + if (geometry != null && geometry instanceof Polygon) { + polygons.add((Polygon) geometry); + } + } + } + if (polygons.size() > 0) { + return geomFactory.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()])); + } else { + return null; + } + default: return null; - } - default: - return null; } } @@ -300,7 +308,7 @@ private LinearRing getOuterLinearRingFromGeometry(Geometry geometry) { /** * Extend the array by copying the first point into the last position - * + * * @param coords original array that is not closed * @return new array one point longer */ @@ -336,36 +344,43 @@ private Geometry decodeGeometryFromWay(Node wayNode, int gtype, int vertices, Ge } decodedCount++; if (overrun) { - System.out.println("Overran expected number of way nodes: " + wayNode + " (" + overrunCount + "/" + decodedCount + ")"); + System.out.println( + "Overran expected number of way nodes: " + wayNode + " (" + overrunCount + "/" + decodedCount + + ")"); } if (coordinates.size() != vertices) { if (vertexMistmaches++ < 10) { - System.err.println("Mismatching vertices size for " + SpatialDatabaseService.convertGeometryTypeToName(gtype) + ":" - + wayNode + ": " + coordinates.size() + " != " + vertices); + System.err.println( + "Mismatching vertices size for " + SpatialDatabaseService.convertGeometryTypeToName(gtype) + ":" + + wayNode + ": " + coordinates.size() + " != " + vertices); } else if (vertexMistmaches % 100 == 0) { System.err.println("Mismatching vertices found " + vertexMistmaches + " times"); } } switch (coordinates.size()) { - case 0: - return null; - case 1: - return geomFactory.createPoint(coordinates.get(0)); - default: - Coordinate[] coords = coordinates.toArray(new Coordinate[0]); - switch (gtype) { - case GTYPE_LINESTRING: - return geomFactory.createLineString(coords); - case GTYPE_POLYGON: - return geomFactory.createPolygon(geomFactory.createLinearRing(coords), new LinearRing[0]); + case 0: + return null; + case 1: + return geomFactory.createPoint(coordinates.get(0)); default: - return geomFactory.createMultiPoint(coords); - } + Coordinate[] coords = coordinates.toArray(new Coordinate[0]); + switch (gtype) { + case GTYPE_LINESTRING: + return geomFactory.createLineString(coords); + case GTYPE_POLYGON: + return geomFactory.createPolygon(geomFactory.createLinearRing(coords), new LinearRing[0]); + default: + return geomFactory.createMultiPoint(coords); + } } } /** - * For OSM data we can build basic geometry shapes as sub-graphs. This code should produce the same kinds of structures that the utilities in the OSMDataset create. However those structures are created from original OSM data, while here we attempt to create equivalent graphs from JTS Geometries. Note that this code is unable to connect the resulting sub-graph into the OSM data model, since the only node it has is the geometry node. Those connections to the rest of the OSM model need to be done in OSMDataset. + * For OSM data we can build basic geometry shapes as sub-graphs. This code should produce the same kinds of + * structures that the utilities in the OSMDataset create. However those structures are created from original OSM + * data, while here we attempt to create equivalent graphs from JTS Geometries. Note that this code is unable to + * connect the resulting sub-graph into the OSM data model, since the only node it has is the geometry node. Those + * connections to the rest of the OSM model need to be done in OSMDataset. */ @Override protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity container) { @@ -373,27 +388,27 @@ protected void encodeGeometryShape(Transaction tx, Geometry geometry, Entity con vertices = 0; int gtype = SpatialDatabaseService.convertJtsClassToGeometryType(geometry.getClass()); switch (gtype) { - case GTYPE_POINT: - makeOSMNode(tx, geometry, geomNode); - break; - case GTYPE_LINESTRING: - case GTYPE_MULTIPOINT: - case GTYPE_POLYGON: - makeOSMWay(tx, geometry, geomNode, gtype); - break; - case GTYPE_MULTILINESTRING: - case GTYPE_MULTIPOLYGON: - int gsubtype = gtype == GTYPE_MULTIPOLYGON ? GTYPE_POLYGON : GTYPE_LINESTRING; - Node relationNode = makeOSMRelation(geometry, geomNode); - int num = geometry.getNumGeometries(); - for (int i = 0; i < num; i++) { - Geometry geom = geometry.getGeometryN(i); - Node wayNode = makeOSMWay(tx, geom, tx.createNode(), gsubtype); - relationNode.createRelationshipTo(wayNode, OSMRelation.MEMBER); - } - break; - default: - throw new SpatialDatabaseException("Unsupported geometry: " + geometry.getClass()); + case GTYPE_POINT: + makeOSMNode(tx, geometry, geomNode); + break; + case GTYPE_LINESTRING: + case GTYPE_MULTIPOINT: + case GTYPE_POLYGON: + makeOSMWay(tx, geometry, geomNode, gtype); + break; + case GTYPE_MULTILINESTRING: + case GTYPE_MULTIPOLYGON: + int gsubtype = gtype == GTYPE_MULTIPOLYGON ? GTYPE_POLYGON : GTYPE_LINESTRING; + Node relationNode = makeOSMRelation(geometry, geomNode); + int num = geometry.getNumGeometries(); + for (int i = 0; i < num; i++) { + Geometry geom = geometry.getGeometryN(i); + Node wayNode = makeOSMWay(tx, geom, tx.createNode(), gsubtype); + relationNode.createRelationshipTo(wayNode, OSMRelation.MEMBER); + } + break; + default: + throw new SpatialDatabaseException("Unsupported geometry: " + geometry.getClass()); } geomNode.setProperty("vertices", vertices); } @@ -450,8 +465,9 @@ private Node makeOSMRelation(Geometry geometry, Node geomNode) { } private String getTimestamp() { - if (dateTimeFormatter == null) + if (dateTimeFormatter == null) { dateTimeFormatter = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + } return dateTimeFormatter.format(new Date(System.currentTimeMillis())); } @@ -460,6 +476,7 @@ private String getTimestamp() { private long missingTags = 0; private class CombinedAttributes { + private Node node; private Entity properties; private HashMap extra = new HashMap<>(); @@ -479,7 +496,7 @@ private class CombinedAttributes { } } catch (NullPointerException e) { if (missingTags++ < 10) { - System.err.println("Geometry has no related tags node: " + geomNode); + System.err.println("Geometry has no related tags node: " + geomNode); } else if (missingTags % 100 == 0) { System.err.println("Geometries without tags found " + missingTags + " times"); } @@ -492,8 +509,9 @@ public boolean hasProperty(String key) { } public Object getProperty(String key) { - return extra.containsKey(key) ? extra.get(key) : node.hasProperty(key) ? node.getProperty(key, null) : properties - .getProperty(key, null); + return extra.containsKey(key) ? extra.get(key) + : node.hasProperty(key) ? node.getProperty(key, null) : properties + .getProperty(key, null); } } @@ -511,9 +529,9 @@ private CombinedAttributes getProperties(Node geomNode) { * This means the default way of storing attributes is simply as properties * of the geometry node. This behaviour can be changed by other domain * models with different encodings. - * + * * @param geomNode node to test - * @param name attribute to check for existence of + * @param name attribute to check for existence of * @return true if node has the specified attribute */ public boolean hasAttribute(Node geomNode, String name) { @@ -526,9 +544,9 @@ public boolean hasAttribute(Node geomNode, String name) { * properties of the geometry node. This behaviour can be changed by other * domain models with different encodings. If the property does not exist, * the method returns null. - * + * * @param geomNode node to test - * @param name attribute to access + * @param name attribute to access * @return attribute value, or null */ public Object getAttribute(Node geomNode, String name) { diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java index c0b0cff8c..0148963b9 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMImporter.java @@ -19,6 +19,8 @@ */ package org.neo4j.gis.spatial.osm; +import static java.util.Arrays.asList; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -71,1652 +73,1696 @@ import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; import org.neo4j.kernel.internal.GraphDatabaseAPI; - -import static java.util.Arrays.asList; - public class OSMImporter implements Constants { - public static DefaultEllipsoid WGS84 = DefaultEllipsoid.WGS84; - public static Label LABEL_DATASET = Label.label("OSMDataset"); - public static Label LABEL_LAYER = Label.label("OSMLayer"); - public static Label LABEL_BBOX = Label.label("OSMBBox"); - public static Label LABEL_CHANGESET = Label.label("OSMChangeset"); - public static Label LABEL_USER = Label.label("OSMUser"); - public static Label LABEL_TAGS = Label.label("OSMTags"); - public static Label LABEL_NODE = Label.label("OSMNode"); - public static Label LABEL_WAY = Label.label("OSMWay"); - public static Label LABEL_WAY_NODE = Label.label("OSMWayNode"); - public static Label LABEL_RELATION = Label.label("OSMRelation"); - public static String PROP_BBOX = "bbox"; - public static String PROP_CHANGESET = "changeset"; - public static String PROP_USER_NAME = "user"; - public static String PROP_USER_ID = "uid"; - public static String PROP_NODE_ID = "node_osm_id"; - public static String PROP_WAY_ID = "way_osm_id"; - public static String PROP_RELATION_ID = "relation_osm_id"; - - protected boolean nodesProcessingFinished = false; - private final String layerName; - private final StatsManager stats = new StatsManager(); - private String osm_dataset = null; - private long missingChangesets = 0; - private final Listener monitor; - private final org.locationtech.jts.geom.Envelope filterEnvelope; - private SecurityContext securityContext = SecurityContext.AUTH_DISABLED; - - private Charset charset = Charset.defaultCharset(); - - private static class TagStats { - private final String name; - private int count = 0; - private final HashMap stats = new HashMap<>(); - - TagStats(String name) { - this.name = name; - } - - int add(String key) { - count++; - if (stats.containsKey(key)) { - int num = stats.get(key); - stats.put(key, ++num); - return num; - } else { - stats.put(key, 1); - return 1; - } - } - - /** - * Return only reasonably commonly used tags. - */ - String[] getTags() { - if (stats.size() > 0) { - int threshold = count / (stats.size() * 20); - ArrayList tags = new ArrayList<>(); - for (String key : stats.keySet()) { - if (stats.get(key) > threshold) tags.add(key); - } - Collections.sort(tags); - return tags.toArray(new String[0]); - } else { - return new String[0]; - } - } - - public String toString() { - return "TagStats[" + name + "]: " + asList(getTags()); - } - } - - private static class StatsManager { - private final HashMap tagStats = new HashMap<>(); - private final HashMap geomStats = new HashMap<>(); - - TagStats getTagStats(String type) { - if (!tagStats.containsKey(type)) { - tagStats.put(type, new TagStats(type)); - } - return tagStats.get(type); - } - - int addToTagStats(String type, String key) { - getTagStats("all").add(key); - return getTagStats(type).add(key); - } - - int addToTagStats(String type, Collection keys) { - int count = 0; - for (String key : keys) { - count += addToTagStats(type, key); - } - return count; - } - - void printTagStats() { - System.out.println("Tag statistics for " + tagStats.size() + " types:"); - for (String key : tagStats.keySet()) { - TagStats stats = tagStats.get(key); - System.out.println("\t" + key + ": " + stats); - } - } - - void addGeomStats(Node geomNode) { - if (geomNode != null) { - addGeomStats((Integer) geomNode.getProperty(PROP_TYPE, null)); - } - } - - void addGeomStats(Integer geom) { - Integer count = geomStats.get(geom); - geomStats.put(geom, count == null ? 1 : count + 1); - } - - void dumpGeomStats() { - System.out.println("Geometry statistics for " + geomStats.size() + " geometry types:"); - for (Integer key : geomStats.keySet()) { - Integer count = geomStats.get(key); - System.out.println("\t" + SpatialDatabaseService.convertGeometryTypeToName(key) + ": " + count); - } - geomStats.clear(); - } - - } - - public OSMImporter(String layerName) { - this(layerName, null); - } - - public OSMImporter(String layerName, Listener monitor) { - this(layerName, null, null); - } - - public OSMImporter(String layerName, Listener monitor, org.locationtech.jts.geom.Envelope filterEnvelope) { - this.layerName = layerName; - if (monitor == null) monitor = new NullListener(); - this.monitor = monitor; - this.filterEnvelope = filterEnvelope; - } - - private Transaction beginTx(GraphDatabaseService database) { - if (!(database instanceof GraphDatabaseAPI)) { - throw new IllegalArgumentException("database must implement GraphDatabaseAPI"); - } - return ((GraphDatabaseAPI) database).beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext); - } - - public long reIndex(GraphDatabaseService database) { - return reIndex(database, 10000, true); - } - - public long reIndex(GraphDatabaseService database, int commitInterval) { - return reIndex(database, commitInterval, true); - } - - public long reIndex(GraphDatabaseService database, int commitInterval, boolean includePoints) { - if (commitInterval < 1) { - throw new IllegalArgumentException("commitInterval must be >= 1"); - } - log("Re-indexing with GraphDatabaseService: " + database + " (class: " + database.getClass() + ")"); - - setLogContext("Index"); - SpatialDatabaseService spatialDatabase = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); - OSMLayer layer; - OSMDataset dataset; - try (Transaction tx = beginTx(database)) { - layer = (OSMLayer) spatialDatabase.getOrCreateLayer(tx, layerName, OSMGeometryEncoder.class, OSMLayer.class); - dataset = OSMDataset.withDatasetId(tx, layer, osm_dataset); - tx.commit(); - } - try (Transaction tx = beginTx(database)) { - layer.clear(tx); // clear the index without destroying underlying data - tx.commit(); - } - - TraversalDescription traversal = new MonoDirectionalTraversalDescription(); - long startTime = System.currentTimeMillis(); - org.neo4j.graphdb.traversal.TraversalDescription findWays = traversal.depthFirst() - .evaluator(Evaluators.excludeStartPosition()) - .relationships(OSMRelation.WAYS, Direction.OUTGOING) - .relationships(OSMRelation.NEXT, Direction.OUTGOING); - org.neo4j.graphdb.traversal.TraversalDescription findNodes = traversal.depthFirst() - .evaluator(Evaluators.excludeStartPosition()) - .relationships(OSMRelation.FIRST_NODE, Direction.OUTGOING) - .relationships(OSMRelation.NEXT, Direction.OUTGOING); - - Transaction tx = beginTx(database); - boolean useWays = missingChangesets > 0; - int count = 0; - try { - layer.setExtraPropertyNames(stats.getTagStats("all").getTags(), tx); - if (useWays) { - beginProgressMonitor(dataset.getWayCount(tx)); - for (Node way : toList(findWays.traverse(tx.getNodeByElementId(osm_dataset)).nodes())) { - updateProgressMonitor(count); - incrLogContext(); - stats.addGeomStats(layer.addWay(tx, way, true)); - if (includePoints) { - long badProxies = 0; - long goodProxies = 0; - for (Node proxy : findNodes.traverse(way).nodes()) { - Relationship nodeRel = proxy.getSingleRelationship(OSMRelation.NODE, Direction.OUTGOING); - if (nodeRel == null) { - badProxies++; - } else { - goodProxies++; - Node node = proxy.getSingleRelationship(OSMRelation.NODE, Direction.OUTGOING).getEndNode(); - stats.addGeomStats(layer.addWay(tx, node, true)); - } - } - if (badProxies > 0) { - System.out.println("Unexpected dangling proxies for way: " + way); - if (way.hasProperty(PROP_WAY_ID)) { - System.out.println("\tWay: " + way.getProperty(PROP_WAY_ID)); - } - System.out.println("\tBad Proxies: " + badProxies); - System.out.println("\tGood Proxies: " + goodProxies); - } - } - if (++count % commitInterval == 0) { - tx.commit(); - tx.close(); - tx = beginTx(database); - } - } // TODO ask charset to user? - } else { - beginProgressMonitor(dataset.getChangesetCount(tx)); - for (Node unsafeNode : toList(dataset.getAllChangesetNodes(tx))) { - WrappedNode changeset = new WrappedNode(unsafeNode); - changeset.refresh(tx); - updateProgressMonitor(count); - incrLogContext(); - try(var relationships = changeset.getRelationships(Direction.INCOMING, OSMRelation.CHANGESET)){ - for (Relationship rel : relationships) { - stats.addGeomStats(layer.addWay(tx, rel.getStartNode(), true)); - } - } - if (++count % commitInterval == 0) { - tx.commit(); - tx.close(); - tx = beginTx(database); - } - } // TODO ask charset to user? - } - tx.commit(); - } finally { - endProgressMonitor(); - tx.close(); - } - - if (verboseLog) { - long stopTime = System.currentTimeMillis(); - log("info | Re-indexing elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000.0)); - stats.dumpGeomStats(); - } - return count; - } - - private List toList(Iterable iterable) { - ArrayList list = new ArrayList<>(); - if (iterable != null) { - for (Node e : iterable) { - list.add(e); - } - } - return list; - } - - private static class GeometryMetaData { - private Envelope bbox = null; - private int vertices = 0; - private int geometry; - - GeometryMetaData(int type) { - this.geometry = type; - } - - public int getGeometryType() { - return geometry; - } - - private void expandToInclude(double[] location) { - if (bbox == null) { - bbox = new Envelope(location); - } else { - bbox.expandToInclude(location); - } - } - - void expandToIncludePoint(double[] location) { - expandToInclude(location); - vertices++; - geometry = -1; - } - - void expandToIncludeBBox(Map nodeProps) { - double[] sbb = (double[]) nodeProps.get(PROP_BBOX); - expandToInclude(new double[]{sbb[0], sbb[2]}); - expandToInclude(new double[]{sbb[1], sbb[3]}); - vertices += (Integer) nodeProps.get("vertices"); - } - - void checkSupportedGeometry(Integer memGType) { - if ((memGType == null || memGType != GTYPE_LINESTRING) - && geometry != GTYPE_POLYGON) { - geometry = -1; - } - } - - void setPolygon() { - geometry = GTYPE_POLYGON; - } - - boolean isValid() { - return geometry > 0; - } - - int getVertices() { - return vertices; - } - - private Envelope getBBox() { - return bbox; - } - } - - private static abstract class OSMWriter { - private static final int UNKNOWN_CHANGESET = -1; - StatsManager statsManager; - OSMImporter osmImporter; - T osm_dataset; - long missingChangesets = 0; - - private OSMWriter(StatsManager statsManager, OSMImporter osmImporter) { - this.statsManager = statsManager; - this.osmImporter = osmImporter; - } - - static OSMWriter fromGraphDatabase(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager stats, OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException { - return new OSMGraphWriter(graphDb, securityContext, stats, osmImporter, txInterval); - } - - protected abstract void startWays(); - - protected abstract void startRelations(); - - protected abstract T getOrCreateOSMDataset(String name); - - protected abstract void setDatasetProperties(Map extractProperties); - - protected abstract void addNodeTags(T node, LinkedHashMap tags, String type); - - protected abstract void addNodeGeometry(T node, int gtype, Envelope bbox, int vertices); - - protected abstract T addNode(Label label, Map properties, String indexKey); - - protected abstract void createRelationship(T from, T to, OSMRelation relType, LinkedHashMap relProps); - - void createRelationship(T from, T to, OSMRelation relType) { - createRelationship(from, to, relType, null); - } - - HashMap stats = new HashMap<>(); - HashMap nodeFindStats = new HashMap<>(); - long logTime = 0; - long findTime = 0; - long firstFindTime = 0; - long lastFindTime = 0; - long firstLogTime = 0; - static int foundNodes = 0; - static int createdNodes = 0; - int foundOSMNodes = 0; - int missingUserCount = 0; - - void logMissingUser(Map nodeProps) { - if (missingUserCount++ < 10) { - System.err.println("Missing user or uid: " + nodeProps.toString()); - } - } - - private class LogCounter { - private long count = 0; - private long totalTime = 0; - } - - void logNodeFoundFrom(String key) { - LogCounter counter = nodeFindStats.computeIfAbsent(key, k -> new LogCounter()); - counter.count++; - foundOSMNodes++; - long currentTime = System.currentTimeMillis(); - if (lastFindTime > 0) { - counter.totalTime += currentTime - lastFindTime; - } - lastFindTime = currentTime; - logNodesFound(currentTime); - } - - void logNodesFound(long currentTime) { - if (firstFindTime == 0) { - firstFindTime = currentTime; - findTime = currentTime; - } - if (currentTime == 0 || currentTime - findTime > 1432) { - int duration = 0; - if (currentTime > 0) { - duration = (int) ((currentTime - firstFindTime) / 1000); - } - System.out.println(new Date(currentTime) + ": Found " - + foundOSMNodes + " nodes during " - + duration + "s way creation: "); - for (String type : nodeFindStats.keySet()) { - LogCounter found = nodeFindStats.get(type); - double rate = 0.0f; - if (found.totalTime > 0) { - rate = (1000.0 * (float) found.count / (float) found.totalTime); - } - System.out.println("\t" + type + ": \t" + found.count - + "/" + (found.totalTime / 1000) - + "s" + " \t(" + rate - + " nodes/second)"); - } - findTime = currentTime; - } - } - - void logNodeAddition(LinkedHashMap tags, - String type) { - Integer count = stats.get(type); - if (count == null) { - count = 1; - } else { - count++; - } - stats.put(type, count); - long currentTime = System.currentTimeMillis(); - if (firstLogTime == 0) { - firstLogTime = currentTime; - logTime = currentTime; - } - if (currentTime - logTime > 1432) { - System.out.println(new Date(currentTime) + ": Saving " + type + " " + count + " \t(" + (1000.0 * (float) count / (float) (currentTime - firstLogTime)) + " " + type + "/second)"); - logTime = currentTime; - } - } - - void describeLoaded() { - logNodesFound(0); - for (String type : new String[]{"node", "way", "relation"}) { - Integer count = stats.get(type); - if (count != null) { - System.out.println("Loaded " + count + " " + type + "s"); - } - } - } - - protected abstract String getDatasetId(); - - private int missingNodeCount = 0; - - private void missingNode(long ndRef) { - if (missingNodeCount++ < 10) { - osmImporter.error("Cannot find node for osm-id " + ndRef); - } - } - - private void describeMissing() { - if (missingNodeCount > 0) { - osmImporter.error("When processing the ways, there were " - + missingNodeCount + " missing nodes"); - } - if (missingMemberCount > 0) { - osmImporter.error("When processing the relations, there were " - + missingMemberCount + " missing members"); - } - } - - private int missingMemberCount = 0; - - private void missingMember(String description) { - if (missingMemberCount++ < 10) { - osmImporter.error("Cannot find member: " + description); - } - } - - T currentNode = null; - T prev_way = null; - T prev_relation = null; - int nodeCount = 0; - int poiCount = 0; - int wayCount = 0; - int relationCount = 0; - int userCount = 0; - int changesetCount = 0; - - /** - * Add the BBox metadata to the dataset - */ - void addOSMBBox(Map bboxProperties) { - T bbox = addNode(LABEL_BBOX, bboxProperties, null); - createRelationship(osm_dataset, bbox, OSMRelation.BBOX); - } - - /** - * Create a new OSM node from the specified attributes (including - * location, user, changeset). The node is stored in the currentNode - * field, so that it can be used in the subsequent call to - * addOSMNodeTags after we close the XML tag for OSM nodes. - * - * @param nodeProps HashMap of attributes for the OSM-node - */ - void createOSMNode(Map nodeProps) { - T userNode = getUserNode(nodeProps); - T changesetNode = getChangesetNode(nodeProps, userNode); - currentNode = addNode(LABEL_NODE, nodeProps, PROP_NODE_ID); - createRelationship(currentNode, changesetNode, OSMRelation.CHANGESET); - nodeCount++; - } - - private void addOSMNodeTags(boolean allPoints, - LinkedHashMap currentNodeTags) { - currentNodeTags.remove("created_by"); // redundant information - // Nodes with tags get added to the index as point geometries - if (allPoints || currentNodeTags.size() > 0) { - Map nodeProps = getNodeProperties(currentNode); - double[] location = new double[]{ - (Double) nodeProps.get("lon"), - (Double) nodeProps.get("lat")}; - addNodeGeometry(currentNode, GTYPE_POINT, new Envelope(location), 1); - poiCount++; - } - addNodeTags(currentNode, currentNodeTags, "node"); - } - - protected void debugNodeWithId(T node, String idName, long[] idValues) { - Map nodeProperties = getNodeProperties(node); - String node_osm_id = nodeProperties.get(idName).toString(); - for (long idValue : idValues) { - if (node_osm_id.equals(Long.toString(idValue))) { - System.out.println("Debug node: " + node_osm_id); - } - } - } - - protected void createOSMWay(Map wayProperties, - ArrayList wayNodes, LinkedHashMap wayTags) { - RoadDirection direction = getRoadDirection(wayTags); - String name = (String) wayTags.get("name"); - int geometry = GTYPE_LINESTRING; - boolean isRoad = wayTags.containsKey("highway"); - if (isRoad) { - wayProperties.put("oneway", direction.toString()); - wayProperties.put("highway", wayTags.get("highway")); - } - if (name != null) { - // Copy name tag to way because this seems like a valuable - // location for - // such a property - wayProperties.put("name", name); - } - T userNode = getUserNode(wayProperties); - T changesetNode = getChangesetNode(wayProperties, userNode); - T way = addNode(LABEL_WAY, wayProperties, PROP_WAY_ID); - createRelationship(way, changesetNode, OSMRelation.CHANGESET); - if (prev_way == null) { - createRelationship(osm_dataset, way, OSMRelation.WAYS); - } else { - createRelationship(prev_way, way, OSMRelation.NEXT); - } - prev_way = way; - addNodeTags(way, wayTags, "way"); - Envelope bbox = null; - T firstNode = null; - T prevNode = null; - T prevProxy = null; - Map prevProps = null; - LinkedHashMap relProps = new LinkedHashMap<>(); - HashMap directionProps = new HashMap<>(); - directionProps.put("oneway", true); - for (long nd_ref : wayNodes) { - T pointNode = getOSMNode(nd_ref, changesetNode); - if (pointNode == null) { - /* - * This can happen if we import not whole planet, so some referenced - * nodes will be unavailable - */ - missingNode(nd_ref); - continue; - } - T proxyNode = createProxyNode(); - if (firstNode == null) { - firstNode = pointNode; - } - if (prevNode == pointNode) { - continue; - } - createRelationship(proxyNode, pointNode, OSMRelation.NODE, null); - Map nodeProps = getNodeProperties(pointNode); - double[] location = new double[]{ - (Double) nodeProps.get("lon"), - (Double) nodeProps.get("lat")}; - if (bbox == null) { - bbox = new Envelope(location); - } else { - bbox.expandToInclude(location); - } - if (prevProxy == null) { - createRelationship(way, proxyNode, OSMRelation.FIRST_NODE); - } else { - relProps.clear(); - double[] prevLoc = new double[]{(Double) prevProps.get("lon"), (Double) prevProps.get("lat")}; - double length = distance(prevLoc[0], prevLoc[1], location[0], location[1]); - relProps.put("length", length); - /* - * We default to bi-directional (and don't store direction in the way node), - * but if it is one-way we mark it as such, and define the direction using the relationship direction - */ - if (direction == RoadDirection.BACKWARD) { - createRelationship(proxyNode, prevProxy, OSMRelation.NEXT, relProps); - } else { - createRelationship(prevProxy, proxyNode, OSMRelation.NEXT, relProps); - } - } - prevNode = pointNode; - prevProxy = proxyNode; - prevProps = nodeProps; - } - if (firstNode != null && prevNode == firstNode) { - geometry = GTYPE_POLYGON; - } - if (wayNodes.size() < 2) { - geometry = GTYPE_POINT; - } - addNodeGeometry(way, geometry, bbox, wayNodes.size()); - this.wayCount++; - } - - private void createOSMRelation(Map relationProperties, - ArrayList> relationMembers, - LinkedHashMap relationTags) { - String name = (String) relationTags.get("name"); - if (name != null) { - /* Copy name tag to way because this seems like a valuable location for such a property */ - relationProperties.put("name", name); - } - T relation = addNode(LABEL_RELATION, relationProperties, PROP_RELATION_ID); - if (prev_relation == null) { - createRelationship(osm_dataset, relation, OSMRelation.RELATIONS); - } else { - createRelationship(prev_relation, relation, OSMRelation.NEXT); - } - prev_relation = relation; - addNodeTags(relation, relationTags, "relation"); - // We will test for cases that invalidate multilinestring further down - GeometryMetaData metaGeom = new GeometryMetaData(GTYPE_MULTILINESTRING); - T prevMember = null; - LinkedHashMap relProps = new LinkedHashMap(); - for (Map memberProps : relationMembers) { - String memberType = (String) memberProps.get("type"); - long member_ref = Long.parseLong(memberProps.get("ref").toString()); - if (memberType != null) { - T member = null; - switch (memberType) { - case "node": - member = getSingleNode(LABEL_NODE, memberType + "_osm_id", member_ref); - break; - case "way": - member = getSingleNode(LABEL_WAY, memberType + "_osm_id", member_ref); - break; - case "relation": - member = getSingleNode(LABEL_RELATION, memberType + "_osm_id", member_ref); - break; - } - if (null == member || prevMember == member) { - /* - * This can happen if we import not whole planet, so some - * referenced nodes will be unavailable - */ - missingMember(memberProps.toString()); - continue; - } - if (member == relation) { - osmImporter.error("Cannot add relation to same member: relation[" + relationTags + "] - member[" + memberProps + "]"); - continue; - } - Map nodeProps = getNodeProperties(member); - if (memberType.equals("node")) { - double[] location = new double[]{(Double) nodeProps.get("lon"), (Double) nodeProps.get("lat")}; - metaGeom.expandToIncludePoint(location); - } else if (memberType.equals("nodes")) { - System.err.println("Unexpected 'nodes' member type"); - } else { - updateGeometryMetaDataFromMember(member, metaGeom, nodeProps); - } - relProps.clear(); - String role = (String) memberProps.get("role"); - if (role != null && role.length() > 0) { - relProps.put("role", role); - if (role.equals("outer")) { - metaGeom.setPolygon(); - } - } - createRelationship(relation, member, OSMRelation.MEMBER, relProps); - prevMember = member; - } else { - System.err.println("Cannot process invalid relation member: " + memberProps.toString()); - } - } - if (metaGeom.isValid()) { - addNodeGeometry(relation, metaGeom.getGeometryType(), - metaGeom.getBBox(), metaGeom.getVertices()); - } - this.relationCount++; - } - - /** - * This method should be overridden by implementation that are able to - * perform database or index optimizations when requested, like the - * batch inserter. - */ - protected abstract void optimize(); - - protected abstract T getSingleNode(Label label, String property, Object value); - - protected abstract Map getNodeProperties(T member); - - protected abstract T getOSMNode(long osmId, T changesetNode); - - protected abstract void updateGeometryMetaDataFromMember(T member, - GeometryMetaData metaGeom, Map nodeProps); - - protected abstract void finish(); - - protected abstract T createProxyNode(); - - protected abstract T getChangesetNode(Map nodeProps, T userNode); - - protected abstract T getUserNode(Map nodeProps); - - } - - private static final class WrappedNode { - private Node inner; - - private WrappedNode(Node inner) { - this.inner = inner; - } - - static WrappedNode fromNode(Node node) { - return node == null ? null : new WrappedNode(node); - } - - void refresh(Transaction tx) { - String id = inner.getElementId(); - inner = tx.getNodeByElementId(id); - if (inner == null) { - throw new IllegalStateException("Failed to find node by id: " + id); - } - } - - Object getProperty(String key) { - return inner.getProperty(key); - } - - Object getProperty(String key, Object defaultValue) { - return inner.getProperty(key, defaultValue); - } - - void setProperty(String key, Object value) { - this.inner.setProperty(key, value); - } - - public String getId() { - return inner.getElementId(); - } - - public Relationship createRelationshipTo(WrappedNode usersNode, OSMRelation users) { - return inner.createRelationshipTo(usersNode.inner, users); - } - - public Relationship createRelationshipTo(Node usersNode, OSMRelation users) { - return inner.createRelationshipTo(usersNode, users); - } - - public Iterable getPropertyKeys() { - return inner.getPropertyKeys(); - } - - public ResourceIterable getRelationships(Direction direction, OSMRelation relType) { - return inner.getRelationships(direction, relType); - } - - public ResourceIterable getRelationships(OSMRelation geom) { - return inner.getRelationships(geom); - } - } - - private static class OSMGraphWriter extends OSMWriter { - private final GraphDatabaseService graphDb; - private final SecurityContext securityContext; - private long currentChangesetId = -1; - private WrappedNode currentChangesetNode; - private long currentUserId = -1; - private WrappedNode currentUserNode; - private WrappedNode usersNode; - private final HashMap changesetNodes = new HashMap<>(); - private Transaction tx; - private int checkCount = 0; - private final int txInterval; - private IndexDefinition nodeIndex; - private IndexDefinition wayIndex; - private IndexDefinition relationIndex; - private IndexDefinition changesetIndex; - private IndexDefinition userIndex; - private final String layerHash; - private final HashMap hashedLabels = new HashMap<>(); - - private OSMGraphWriter(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager statsManager, OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException { - super(statsManager, osmImporter); - this.graphDb = graphDb; - this.securityContext = securityContext; - this.txInterval = txInterval; - if (this.txInterval < 100) { - System.err.println("Warning: Unusually short txInterval, expect bad insert performance"); - } - this.layerHash = md5Hash(osmImporter.layerName); - checkTx(null); // Opens transaction for future writes - } - - private static String md5Hash(String text) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("MD5"); - md.update(text.getBytes()); - byte[] digest = md.digest(); - String hashed = DatatypeConverter.printHexBinary(digest).toUpperCase(); - return hashed; - } - - private void successTx() { - if (tx != null) { - tx.commit(); - tx.close(); - tx = null; - checkCount = 0; - } - } - - private Transaction beginTx(GraphDatabaseService database) { - return beginTx(database, securityContext); - } - - private Transaction beginIndexTx(GraphDatabaseService database) { - return beginTx(database, IndexManager.IndexAccessMode.withIndexCreate(securityContext)); - } - - private static Transaction beginTx(GraphDatabaseService database, SecurityContext securityContext) { - if (!(database instanceof GraphDatabaseAPI)) { - throw new IllegalArgumentException("database must implement GraphDatabaseAPI"); - } - return ((GraphDatabaseAPI) database).beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext); - } - - private void beginTx() { - tx = beginTx(graphDb); - recoverNode(osm_dataset); - recoverNode(currentNode); - recoverNode(prev_relation); - recoverNode(prev_way); - recoverNode(currentChangesetNode); - recoverNode(currentUserNode); - recoverNode(usersNode); - changesetNodes.forEach((id, node) -> node.refresh(tx)); - } - - private WrappedNode checkTx(WrappedNode previous) { - if (checkCount++ > txInterval || tx == null || checkCount > 10) { - successTx(); - beginTx(); - recoverNode(previous); - } - return previous; - } - - private void recoverNode(WrappedNode outOfTx) { - if (outOfTx != null) { - outOfTx.refresh(tx); - } - } - - private WrappedNode findNodeByName(Label label, String name) { - Node node = findNodeByLabelProperty(tx, label, "name", name); - if (node != null) { - return WrappedNode.fromNode(node); - } - return null; - } - - private WrappedNode createNodeWithLabel(Transaction tx, Label label) { - Label hashed = getLabelHashed(label); - return WrappedNode.fromNode(tx.createNode(label, hashed)); - } - - @Override - protected void startWays() { - System.out.println("About to create node index"); - nodeIndex = createIndex(LABEL_NODE, PROP_NODE_ID); - System.out.println("About to populate node index"); - // TODO: Should we use another TX? - tx.schema().awaitIndexOnline(nodeIndex, 1, TimeUnit.MINUTES); // could be a large index - System.out.println("Finished populating node index"); - } - - @Override - protected void startRelations() { - System.out.println("About to create way and relation indexes"); - wayIndex = createIndex(LABEL_WAY, PROP_WAY_ID); - relationIndex = createIndex(LABEL_RELATION, PROP_RELATION_ID); - System.out.println("About to populate way and relation indexes"); - // TODO: Should we use another TX? - tx.schema().awaitIndexOnline(wayIndex, 1, TimeUnit.MINUTES); - tx.schema().awaitIndexOnline(nodeIndex, 1, TimeUnit.MINUTES); - System.out.println("Finished populating way and relation indexes"); - } - - protected void optimize() { - for (IndexDefinition index : new IndexDefinition[]{nodeIndex, wayIndex, relationIndex}) { - if (index != null) { - tx.schema().awaitIndexOnline(index, 30, TimeUnit.MINUTES); - } - } - } - - private Label getLabelHashed(Label label) { - if (hashedLabels.containsKey(label)) { - return hashedLabels.get(label); - } else { - Label hashed = Label.label(label.name() + "_" + layerHash); - hashedLabels.put(label, hashed); - return hashed; - } - } - - private Node findNodeByLabelProperty(Transaction tx, Label label, String propertyKey, Object value) { - Label hashed = getLabelHashed(label); - return tx.findNode(hashed, propertyKey, value); - } - - private IndexDefinition createIndex(Label label, String propertyKey) { - Label hashed = getLabelHashed(label); - String indexName = String.format("OSM-%s-%s-%s", osmImporter.layerName, hashed.name(), propertyKey); - IndexDefinition index = findIndex(tx, indexName, hashed, propertyKey); - if (index == null) { - successTx(); - try (Transaction indexTx = beginIndexTx(graphDb)) { - index = indexTx.schema().indexFor(hashed).on(propertyKey).withName(indexName).create(); - indexTx.commit(); - } - System.out.println("Created index " + index.getName()); - beginTx(); - } - return index; - } - - private IndexDefinition createIndexIfNotNull(IndexDefinition index, Label label, String propertyKey) { - if (index == null) { - index = createIndex(label, propertyKey); - tx.schema().awaitIndexOnline(index, 1, TimeUnit.MINUTES); // small index should be fast - } - return index; - } - - private IndexDefinition findIndex(Transaction tx, String indexName, Label label, String propertyKey) { - for (IndexDefinition index : tx.schema().getIndexes(label)) { - for (String prop : index.getPropertyKeys()) { - if (prop.equals(propertyKey)) { - if (index.getName().equals(indexName)) { - return index; - } else { - throw new IllegalStateException(String.format("Found pre-existing index '%s' for index '%s'", index.getName(), indexName)); - } - } - } - } - return null; - } - - private WrappedNode getOrCreateNode(Label label, String name, String type) { - WrappedNode node = findNodeByName(label, name); - if (node == null) { - WrappedNode n = createNodeWithLabel(tx, label); - n.setProperty("name", name); - n.setProperty("type", type); - node = checkTx(n); - } - return node; - } - - @Override - protected WrappedNode getOrCreateOSMDataset(String name) { - if (osm_dataset == null) { - osm_dataset = getOrCreateNode(LABEL_DATASET, name, "osm"); - } - return osm_dataset; - } - - @Override - protected void setDatasetProperties(Map extractProperties) { - for (String key : extractProperties.keySet()) { - osm_dataset.setProperty(key, extractProperties.get(key)); - } - } - - private void addProperties(Entity node, Map properties) { - for (String property : properties.keySet()) { - node.setProperty(property, properties.get(property)); - } - } - - @Override - protected void addNodeTags(WrappedNode node, LinkedHashMap tags, String type) { - logNodeAddition(tags, type); - if (node != null && tags.size() > 0) { - statsManager.addToTagStats(type, tags.keySet()); - WrappedNode tagsNode = createNodeWithLabel(tx, LABEL_TAGS); - addProperties(tagsNode.inner, tags); - node.createRelationshipTo(tagsNode, OSMRelation.TAGS); - tags.clear(); - } - } - - @Override - protected void addNodeGeometry(WrappedNode node, int gtype, Envelope bbox, int vertices) { - if (node != null && bbox != null && vertices > 0) { - if (gtype == GTYPE_GEOMETRY) gtype = vertices > 1 ? GTYPE_MULTIPOINT : GTYPE_POINT; - Node geomNode = tx.createNode(); - geomNode.setProperty("gtype", gtype); - geomNode.setProperty("vertices", vertices); - geomNode.setProperty(PROP_BBOX, new double[]{bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()}); - node.createRelationshipTo(geomNode, OSMRelation.GEOM); - statsManager.addGeomStats(gtype); - } - } - - @Override - protected WrappedNode addNode(Label label, Map properties, String indexKey) { - WrappedNode node = createNodeWithLabel(tx, label); - if (indexKey != null && properties.containsKey(indexKey)) { - properties.put(indexKey, Long.parseLong(properties.get(indexKey).toString())); - } - addProperties(node.inner, properties); - return checkTx(node); - } - - @Override - protected void createRelationship(WrappedNode from, WrappedNode to, OSMRelation relType, LinkedHashMap relProps) { - if (from != null & to != null) { - Relationship rel = from.createRelationshipTo(to, relType); - if (relProps != null && relProps.size() > 0) { - addProperties(rel, relProps); - } - } - } - - @Override - protected String getDatasetId() { - return osm_dataset.getId(); - } - - @Override - protected WrappedNode getSingleNode(Label label, String property, Object value) { - Node node = findNodeByLabelProperty(tx, LABEL_NODE, property, value); - return node == null ? null : WrappedNode.fromNode(node); - } - - @Override - protected Map getNodeProperties(WrappedNode node) { - LinkedHashMap properties = new LinkedHashMap<>(); - for (String property : node.getPropertyKeys()) { - properties.put(property, node.getProperty(property)); - } - return properties; - } - - @Override - protected WrappedNode getOSMNode(long osmId, WrappedNode changesetNode) { - if (currentChangesetNode != changesetNode || changesetNodes.isEmpty()) { - currentChangesetNode = changesetNode; - changesetNodes.clear(); - if (changesetNode != null) { - try (var relationships = changesetNode.getRelationships(Direction.INCOMING, OSMRelation.CHANGESET)) { - for (Relationship rel : relationships) { - Node node = rel.getStartNode(); - Long nodeOsmId = (Long) node.getProperty(PROP_NODE_ID, null); - if (nodeOsmId != null) { - changesetNodes.put(nodeOsmId, WrappedNode.fromNode(node)); - } - } - } - } - } - WrappedNode node = changesetNodes.get(osmId); - if (node == null) { - logNodeFoundFrom("node-index"); - node = WrappedNode.fromNode(findNodeByLabelProperty(tx, LABEL_NODE, PROP_NODE_ID, osmId)); - } else { - logNodeFoundFrom(PROP_CHANGESET); - } - return node; - } - - @Override - protected void updateGeometryMetaDataFromMember(WrappedNode member, GeometryMetaData metaGeom, Map nodeProps) { - try (var relationships = member.getRelationships(OSMRelation.GEOM)) { - for (Relationship rel : relationships) { - nodeProps = getNodeProperties(WrappedNode.fromNode(rel.getEndNode())); - metaGeom.checkSupportedGeometry((Integer) nodeProps.get("gtype")); - metaGeom.expandToIncludeBBox(nodeProps); - } - } - } - - @Override - protected void finish() { - if (tx == null) beginTx(); - osm_dataset.setProperty("relationCount", (Integer) osm_dataset.getProperty("relationCount", 0) + relationCount); - osm_dataset.setProperty("wayCount", (Integer) osm_dataset.getProperty("wayCount", 0) + wayCount); - osm_dataset.setProperty("nodeCount", (Integer) osm_dataset.getProperty("nodeCount", 0) + nodeCount); - osm_dataset.setProperty("poiCount", (Integer) osm_dataset.getProperty("poiCount", 0) + poiCount); - osm_dataset.setProperty("changesetCount", (Integer) osm_dataset.getProperty("changesetCount", 0) + changesetCount); - osm_dataset.setProperty("userCount", (Integer) osm_dataset.getProperty("userCount", 0) + userCount); - successTx(); - } - - @Override - protected WrappedNode createProxyNode() { - return WrappedNode.fromNode(tx.createNode(LABEL_WAY_NODE)); - } - - @Override - protected WrappedNode getChangesetNode(Map nodeProps, WrappedNode userNode) { - Object changesetObj = nodeProps.remove(PROP_CHANGESET); - if (changesetObj != null) { - long changeset = Long.parseLong(changesetObj.toString()); - if (changeset != currentChangesetId) { - changesetIndex = createIndexIfNotNull(changesetIndex, LABEL_CHANGESET, PROP_CHANGESET); - currentChangesetId = changeset; - Node changesetNode = findNodeByLabelProperty(tx, LABEL_CHANGESET, PROP_CHANGESET, currentChangesetId); - if (changesetNode != null) { - currentChangesetNode = WrappedNode.fromNode(changesetNode); - } else { - LinkedHashMap changesetProps = new LinkedHashMap<>(); - changesetProps.put(PROP_CHANGESET, currentChangesetId); - changesetProps.put("timestamp", nodeProps.get("timestamp")); - currentChangesetNode = addNode(LABEL_CHANGESET, changesetProps, PROP_CHANGESET); - changesetCount++; - if (userNode != null) { - createRelationship(currentChangesetNode, userNode, OSMRelation.USER); - } - } - } - } else { - currentChangesetId = OSMWriter.UNKNOWN_CHANGESET; - currentChangesetNode = null; - missingChangesets++; - } - return currentChangesetNode; - } - - @Override - protected WrappedNode getUserNode(Map nodeProps) { - try { - long uid = Long.parseLong(nodeProps.remove(PROP_USER_ID).toString()); - String name = nodeProps.remove(PROP_USER_NAME).toString(); - if (uid != currentUserId) { - currentUserId = uid; - userIndex = createIndexIfNotNull(userIndex, LABEL_USER, PROP_USER_ID); - Node userNode = findNodeByLabelProperty(tx, LABEL_USER, PROP_USER_ID, currentUserId); - if (userNode != null) { - currentUserNode = WrappedNode.fromNode(userNode); - } else { - LinkedHashMap userProps = new LinkedHashMap<>(); - userProps.put(PROP_USER_ID, currentUserId); - userProps.put("name", name); - userProps.put("timestamp", nodeProps.get("timestamp")); - currentUserNode = addNode(LABEL_USER, userProps, PROP_USER_ID); - userCount++; - if (usersNode == null) { - usersNode = createNodeWithLabel(tx, LABEL_USER); - osm_dataset.createRelationshipTo(usersNode, OSMRelation.USERS); - } - usersNode.createRelationshipTo(currentUserNode, OSMRelation.OSM_USER); - } - } - } catch (Exception e) { - currentUserId = -1; - currentUserNode = null; - logMissingUser(nodeProps); - } - return currentUserNode; - } - - public String toString() { - return "OSMGraphWriter: DatabaseService[" + graphDb + "]:txInterval[" + this.txInterval + "]"; - } - - } - - public void importFile(GraphDatabaseService database, String dataset) throws Exception { - importFile(database, dataset, false, 5000); - } - - public void importFile(GraphDatabaseService database, String dataset, int txInterval) throws Exception { - importFile(database, dataset, false, txInterval); - } - - public void importFile(GraphDatabaseService database, String dataset, boolean allPoints, int txInterval) throws Exception { - importFile(OSMWriter.fromGraphDatabase(database, securityContext, stats, this, txInterval), dataset, allPoints, charset); - } - - public static class CountedFileReader extends InputStreamReader { - private long length = 0; - private long charsRead = 0; - - public CountedFileReader(String path, Charset charset) throws FileNotFoundException { - super(new FileInputStream(path), charset); - this.length = (new File(path)).length(); - } - - public long getCharsRead() { - return charsRead; - } - - public long getlength() { - return length; - } - - public double getProgress() { - return length > 0 ? (double) charsRead / (double) length : 0; - } - - public int getPercentRead() { - return (int) (100.0 * getProgress()); - } - - public int read(char[] cbuf, int offset, int length) - throws IOException { - int read = super.read(cbuf, offset, length); - if (read > 0) charsRead += read; - return read; - } - } - - private int progress = 0; - private long progressTime = 0; - - private void beginProgressMonitor(int length) { - monitor.begin(length); - progress = 0; - progressTime = System.currentTimeMillis(); - } - - private void updateProgressMonitor(int currentProgress) { - if (currentProgress > this.progress) { - long time = System.currentTimeMillis(); - if (time - progressTime > 1000) { - monitor.worked(currentProgress - progress); - progress = currentProgress; - progressTime = time; - } - } - } - - private void endProgressMonitor() { - monitor.done(); - progress = 0; - progressTime = 0; - } - - public void setSecurityContext(SecurityContext securityContext) { - this.securityContext = securityContext; - } - - public void setCharset(Charset charset) { - this.charset = charset; - } - - public void importFile(OSMWriter osmWriter, String dataset, boolean allPoints, Charset charset) throws IOException, XMLStreamException { - log("Importing with osm-writer: " + osmWriter); - osmWriter.getOrCreateOSMDataset(layerName); - osm_dataset = osmWriter.getDatasetId(); - - long startTime = System.currentTimeMillis(); - long[] times = new long[]{0L, 0L, 0L, 0L}; - javax.xml.stream.XMLInputFactory factory = javax.xml.stream.XMLInputFactory.newInstance(); - CountedFileReader reader = new CountedFileReader(dataset, charset); - javax.xml.stream.XMLStreamReader parser = factory.createXMLStreamReader(reader); - int countXMLTags = 0; - beginProgressMonitor(100); - setLogContext(dataset); - boolean startedWays = false; - boolean startedRelations = false; - try { - ArrayList currentXMLTags = new ArrayList<>(); - int depth = 0; - Map wayProperties = null; - ArrayList wayNodes = new ArrayList<>(); - Map relationProperties = null; - ArrayList> relationMembers = new ArrayList<>(); - LinkedHashMap currentNodeTags = new LinkedHashMap<>(); - while (true) { - updateProgressMonitor(reader.getPercentRead()); - incrLogContext(); - int event = parser.next(); - if (event == javax.xml.stream.XMLStreamConstants.END_DOCUMENT) { - break; - } - switch (event) { - case javax.xml.stream.XMLStreamConstants.START_ELEMENT: - currentXMLTags.add(depth, parser.getLocalName()); - String tagPath = currentXMLTags.toString(); - if (tagPath.equals("[osm]")) { - osmWriter.setDatasetProperties(extractProperties(parser)); - } else if (tagPath.equals("[osm, bounds]")) { - osmWriter.addOSMBBox(extractProperties(PROP_BBOX, parser)); - } else if (tagPath.equals("[osm, node]")) { - /* */ - boolean includeNode = true; - Map nodeProperties = extractProperties("node", parser); - if (filterEnvelope != null) { - includeNode = filterEnvelope.contains((Double) nodeProperties.get("lon"), (Double) nodeProperties.get("lat")); - } - if (includeNode) { - osmWriter.createOSMNode(nodeProperties); - } - } else if (tagPath.equals("[osm, way]")) { - /* */ - if (!startedWays) { - startedWays = true; - osmWriter.startWays(); - times[0] = System.currentTimeMillis(); - osmWriter.optimize(); - times[1] = System.currentTimeMillis(); - } - wayProperties = extractProperties("way", parser); - wayNodes.clear(); - } else if (tagPath.equals("[osm, way, nd]")) { - Map properties = extractProperties(parser); - wayNodes.add(Long.parseLong(properties.get("ref").toString())); - } else if (tagPath.endsWith("tag]")) { - Map properties = extractProperties(parser); - currentNodeTags.put(properties.get("k").toString(), - properties.get("v").toString()); - } else if (tagPath.equals("[osm, relation]")) { - /* */ - if (!startedRelations) { - startedRelations = true; - osmWriter.startRelations(); - times[2] = System.currentTimeMillis(); - osmWriter.optimize(); - times[3] = System.currentTimeMillis(); - } - relationProperties = extractProperties("relation", parser); - relationMembers.clear(); - } else if (tagPath.equals("[osm, relation, member]")) { - relationMembers.add(extractProperties(parser)); - } - if (startedRelations) { - if (countXMLTags < 10) { - debug("Starting tag at depth " + depth + ": " - + currentXMLTags.get(depth) + " - " - + currentXMLTags.toString()); - for (int i = 0; i < parser.getAttributeCount(); i++) { - debug("\t" + currentXMLTags.toString() + ": " - + parser.getAttributeLocalName(i) + "[" - + parser.getAttributeNamespace(i) + "," - + parser.getAttributePrefix(i) + "," - + parser.getAttributeType(i) + "," - + "] = " + parser.getAttributeValue(i)); - } - } - countXMLTags++; - } - depth++; - break; - case javax.xml.stream.XMLStreamConstants.END_ELEMENT: - switch (currentXMLTags.toString()) { - case "[osm, node]": - osmWriter.addOSMNodeTags(allPoints, currentNodeTags); - break; - case "[osm, way]": - osmWriter.createOSMWay(wayProperties, wayNodes, currentNodeTags); - break; - case "[osm, relation]": - osmWriter.createOSMRelation(relationProperties, relationMembers, currentNodeTags); - break; - } - depth--; - currentXMLTags.remove(depth); - break; - default: - break; - } - } - } finally { - endProgressMonitor(); - parser.close(); - osmWriter.finish(); - this.osm_dataset = osmWriter.getDatasetId(); - this.missingChangesets = osmWriter.missingChangesets; - } - if (verboseLog) { - describeTimes(startTime, times); - osmWriter.describeMissing(); - osmWriter.describeLoaded(); - - long stopTime = System.currentTimeMillis(); - log("info | Elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000.0)); - stats.dumpGeomStats(); - stats.printTagStats(); - } - } - - private void describeTimes(long startTime, long[] times) { - long endTime = System.currentTimeMillis(); - log("Completed load in " + (1.0 * (endTime - startTime) / 1000.0) + "s"); - log("\tImported nodes: " + (1.0 * (times[0] - startTime) / 1000.0) + "s"); - log("\tOptimized index: " + (1.0 * (times[1] - times[0]) / 1000.0) + "s"); - log("\tImported ways: " + (1.0 * (times[2] - times[1]) / 1000.0) + "s"); - log("\tOptimized index: " + (1.0 * (times[3] - times[2]) / 1000.0) + "s"); - log("\tImported rels: " + (1.0 * (endTime - times[3]) / 1000.0) + "s"); - } - - private Map extractProperties(XMLStreamReader parser) { - return extractProperties(null, parser); - } - - private Map extractProperties(String name, XMLStreamReader parser) { + + public static DefaultEllipsoid WGS84 = DefaultEllipsoid.WGS84; + public static Label LABEL_DATASET = Label.label("OSMDataset"); + public static Label LABEL_LAYER = Label.label("OSMLayer"); + public static Label LABEL_BBOX = Label.label("OSMBBox"); + public static Label LABEL_CHANGESET = Label.label("OSMChangeset"); + public static Label LABEL_USER = Label.label("OSMUser"); + public static Label LABEL_TAGS = Label.label("OSMTags"); + public static Label LABEL_NODE = Label.label("OSMNode"); + public static Label LABEL_WAY = Label.label("OSMWay"); + public static Label LABEL_WAY_NODE = Label.label("OSMWayNode"); + public static Label LABEL_RELATION = Label.label("OSMRelation"); + public static String PROP_BBOX = "bbox"; + public static String PROP_CHANGESET = "changeset"; + public static String PROP_USER_NAME = "user"; + public static String PROP_USER_ID = "uid"; + public static String PROP_NODE_ID = "node_osm_id"; + public static String PROP_WAY_ID = "way_osm_id"; + public static String PROP_RELATION_ID = "relation_osm_id"; + + protected boolean nodesProcessingFinished = false; + private final String layerName; + private final StatsManager stats = new StatsManager(); + private String osm_dataset = null; + private long missingChangesets = 0; + private final Listener monitor; + private final org.locationtech.jts.geom.Envelope filterEnvelope; + private SecurityContext securityContext = SecurityContext.AUTH_DISABLED; + + private Charset charset = Charset.defaultCharset(); + + private static class TagStats { + + private final String name; + private int count = 0; + private final HashMap stats = new HashMap<>(); + + TagStats(String name) { + this.name = name; + } + + int add(String key) { + count++; + if (stats.containsKey(key)) { + int num = stats.get(key); + stats.put(key, ++num); + return num; + } else { + stats.put(key, 1); + return 1; + } + } + + /** + * Return only reasonably commonly used tags. + */ + String[] getTags() { + if (stats.size() > 0) { + int threshold = count / (stats.size() * 20); + ArrayList tags = new ArrayList<>(); + for (String key : stats.keySet()) { + if (stats.get(key) > threshold) { + tags.add(key); + } + } + Collections.sort(tags); + return tags.toArray(new String[0]); + } else { + return new String[0]; + } + } + + public String toString() { + return "TagStats[" + name + "]: " + asList(getTags()); + } + } + + private static class StatsManager { + + private final HashMap tagStats = new HashMap<>(); + private final HashMap geomStats = new HashMap<>(); + + TagStats getTagStats(String type) { + if (!tagStats.containsKey(type)) { + tagStats.put(type, new TagStats(type)); + } + return tagStats.get(type); + } + + int addToTagStats(String type, String key) { + getTagStats("all").add(key); + return getTagStats(type).add(key); + } + + int addToTagStats(String type, Collection keys) { + int count = 0; + for (String key : keys) { + count += addToTagStats(type, key); + } + return count; + } + + void printTagStats() { + System.out.println("Tag statistics for " + tagStats.size() + " types:"); + for (String key : tagStats.keySet()) { + TagStats stats = tagStats.get(key); + System.out.println("\t" + key + ": " + stats); + } + } + + void addGeomStats(Node geomNode) { + if (geomNode != null) { + addGeomStats((Integer) geomNode.getProperty(PROP_TYPE, null)); + } + } + + void addGeomStats(Integer geom) { + Integer count = geomStats.get(geom); + geomStats.put(geom, count == null ? 1 : count + 1); + } + + void dumpGeomStats() { + System.out.println("Geometry statistics for " + geomStats.size() + " geometry types:"); + for (Integer key : geomStats.keySet()) { + Integer count = geomStats.get(key); + System.out.println("\t" + SpatialDatabaseService.convertGeometryTypeToName(key) + ": " + count); + } + geomStats.clear(); + } + + } + + public OSMImporter(String layerName) { + this(layerName, null); + } + + public OSMImporter(String layerName, Listener monitor) { + this(layerName, null, null); + } + + public OSMImporter(String layerName, Listener monitor, org.locationtech.jts.geom.Envelope filterEnvelope) { + this.layerName = layerName; + if (monitor == null) { + monitor = new NullListener(); + } + this.monitor = monitor; + this.filterEnvelope = filterEnvelope; + } + + private Transaction beginTx(GraphDatabaseService database) { + if (!(database instanceof GraphDatabaseAPI)) { + throw new IllegalArgumentException("database must implement GraphDatabaseAPI"); + } + return ((GraphDatabaseAPI) database).beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext); + } + + public long reIndex(GraphDatabaseService database) { + return reIndex(database, 10000, true); + } + + public long reIndex(GraphDatabaseService database, int commitInterval) { + return reIndex(database, commitInterval, true); + } + + public long reIndex(GraphDatabaseService database, int commitInterval, boolean includePoints) { + if (commitInterval < 1) { + throw new IllegalArgumentException("commitInterval must be >= 1"); + } + log("Re-indexing with GraphDatabaseService: " + database + " (class: " + database.getClass() + ")"); + + setLogContext("Index"); + SpatialDatabaseService spatialDatabase = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) database, SecurityContext.AUTH_DISABLED)); + OSMLayer layer; + OSMDataset dataset; + try (Transaction tx = beginTx(database)) { + layer = (OSMLayer) spatialDatabase.getOrCreateLayer(tx, layerName, OSMGeometryEncoder.class, + OSMLayer.class); + dataset = OSMDataset.withDatasetId(tx, layer, osm_dataset); + tx.commit(); + } + try (Transaction tx = beginTx(database)) { + layer.clear(tx); // clear the index without destroying underlying data + tx.commit(); + } + + TraversalDescription traversal = new MonoDirectionalTraversalDescription(); + long startTime = System.currentTimeMillis(); + org.neo4j.graphdb.traversal.TraversalDescription findWays = traversal.depthFirst() + .evaluator(Evaluators.excludeStartPosition()) + .relationships(OSMRelation.WAYS, Direction.OUTGOING) + .relationships(OSMRelation.NEXT, Direction.OUTGOING); + org.neo4j.graphdb.traversal.TraversalDescription findNodes = traversal.depthFirst() + .evaluator(Evaluators.excludeStartPosition()) + .relationships(OSMRelation.FIRST_NODE, Direction.OUTGOING) + .relationships(OSMRelation.NEXT, Direction.OUTGOING); + + Transaction tx = beginTx(database); + boolean useWays = missingChangesets > 0; + int count = 0; + try { + layer.setExtraPropertyNames(stats.getTagStats("all").getTags(), tx); + if (useWays) { + beginProgressMonitor(dataset.getWayCount(tx)); + for (Node way : toList(findWays.traverse(tx.getNodeByElementId(osm_dataset)).nodes())) { + updateProgressMonitor(count); + incrLogContext(); + stats.addGeomStats(layer.addWay(tx, way, true)); + if (includePoints) { + long badProxies = 0; + long goodProxies = 0; + for (Node proxy : findNodes.traverse(way).nodes()) { + Relationship nodeRel = proxy.getSingleRelationship(OSMRelation.NODE, Direction.OUTGOING); + if (nodeRel == null) { + badProxies++; + } else { + goodProxies++; + Node node = proxy.getSingleRelationship(OSMRelation.NODE, Direction.OUTGOING) + .getEndNode(); + stats.addGeomStats(layer.addWay(tx, node, true)); + } + } + if (badProxies > 0) { + System.out.println("Unexpected dangling proxies for way: " + way); + if (way.hasProperty(PROP_WAY_ID)) { + System.out.println("\tWay: " + way.getProperty(PROP_WAY_ID)); + } + System.out.println("\tBad Proxies: " + badProxies); + System.out.println("\tGood Proxies: " + goodProxies); + } + } + if (++count % commitInterval == 0) { + tx.commit(); + tx.close(); + tx = beginTx(database); + } + } // TODO ask charset to user? + } else { + beginProgressMonitor(dataset.getChangesetCount(tx)); + for (Node unsafeNode : toList(dataset.getAllChangesetNodes(tx))) { + WrappedNode changeset = new WrappedNode(unsafeNode); + changeset.refresh(tx); + updateProgressMonitor(count); + incrLogContext(); + try (var relationships = changeset.getRelationships(Direction.INCOMING, OSMRelation.CHANGESET)) { + for (Relationship rel : relationships) { + stats.addGeomStats(layer.addWay(tx, rel.getStartNode(), true)); + } + } + if (++count % commitInterval == 0) { + tx.commit(); + tx.close(); + tx = beginTx(database); + } + } // TODO ask charset to user? + } + tx.commit(); + } finally { + endProgressMonitor(); + tx.close(); + } + + if (verboseLog) { + long stopTime = System.currentTimeMillis(); + log("info | Re-indexing elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000.0)); + stats.dumpGeomStats(); + } + return count; + } + + private List toList(Iterable iterable) { + ArrayList list = new ArrayList<>(); + if (iterable != null) { + for (Node e : iterable) { + list.add(e); + } + } + return list; + } + + private static class GeometryMetaData { + + private Envelope bbox = null; + private int vertices = 0; + private int geometry; + + GeometryMetaData(int type) { + this.geometry = type; + } + + public int getGeometryType() { + return geometry; + } + + private void expandToInclude(double[] location) { + if (bbox == null) { + bbox = new Envelope(location); + } else { + bbox.expandToInclude(location); + } + } + + void expandToIncludePoint(double[] location) { + expandToInclude(location); + vertices++; + geometry = -1; + } + + void expandToIncludeBBox(Map nodeProps) { + double[] sbb = (double[]) nodeProps.get(PROP_BBOX); + expandToInclude(new double[]{sbb[0], sbb[2]}); + expandToInclude(new double[]{sbb[1], sbb[3]}); + vertices += (Integer) nodeProps.get("vertices"); + } + + void checkSupportedGeometry(Integer memGType) { + if ((memGType == null || memGType != GTYPE_LINESTRING) + && geometry != GTYPE_POLYGON) { + geometry = -1; + } + } + + void setPolygon() { + geometry = GTYPE_POLYGON; + } + + boolean isValid() { + return geometry > 0; + } + + int getVertices() { + return vertices; + } + + private Envelope getBBox() { + return bbox; + } + } + + private static abstract class OSMWriter { + + private static final int UNKNOWN_CHANGESET = -1; + StatsManager statsManager; + OSMImporter osmImporter; + T osm_dataset; + long missingChangesets = 0; + + private OSMWriter(StatsManager statsManager, OSMImporter osmImporter) { + this.statsManager = statsManager; + this.osmImporter = osmImporter; + } + + static OSMWriter fromGraphDatabase(GraphDatabaseService graphDb, SecurityContext securityContext, + StatsManager stats, OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException { + return new OSMGraphWriter(graphDb, securityContext, stats, osmImporter, txInterval); + } + + protected abstract void startWays(); + + protected abstract void startRelations(); + + protected abstract T getOrCreateOSMDataset(String name); + + protected abstract void setDatasetProperties(Map extractProperties); + + protected abstract void addNodeTags(T node, LinkedHashMap tags, String type); + + protected abstract void addNodeGeometry(T node, int gtype, Envelope bbox, int vertices); + + protected abstract T addNode(Label label, Map properties, String indexKey); + + protected abstract void createRelationship(T from, T to, OSMRelation relType, + LinkedHashMap relProps); + + void createRelationship(T from, T to, OSMRelation relType) { + createRelationship(from, to, relType, null); + } + + HashMap stats = new HashMap<>(); + HashMap nodeFindStats = new HashMap<>(); + long logTime = 0; + long findTime = 0; + long firstFindTime = 0; + long lastFindTime = 0; + long firstLogTime = 0; + static int foundNodes = 0; + static int createdNodes = 0; + int foundOSMNodes = 0; + int missingUserCount = 0; + + void logMissingUser(Map nodeProps) { + if (missingUserCount++ < 10) { + System.err.println("Missing user or uid: " + nodeProps.toString()); + } + } + + private class LogCounter { + + private long count = 0; + private long totalTime = 0; + } + + void logNodeFoundFrom(String key) { + LogCounter counter = nodeFindStats.computeIfAbsent(key, k -> new LogCounter()); + counter.count++; + foundOSMNodes++; + long currentTime = System.currentTimeMillis(); + if (lastFindTime > 0) { + counter.totalTime += currentTime - lastFindTime; + } + lastFindTime = currentTime; + logNodesFound(currentTime); + } + + void logNodesFound(long currentTime) { + if (firstFindTime == 0) { + firstFindTime = currentTime; + findTime = currentTime; + } + if (currentTime == 0 || currentTime - findTime > 1432) { + int duration = 0; + if (currentTime > 0) { + duration = (int) ((currentTime - firstFindTime) / 1000); + } + System.out.println(new Date(currentTime) + ": Found " + + foundOSMNodes + " nodes during " + + duration + "s way creation: "); + for (String type : nodeFindStats.keySet()) { + LogCounter found = nodeFindStats.get(type); + double rate = 0.0f; + if (found.totalTime > 0) { + rate = (1000.0 * (float) found.count / (float) found.totalTime); + } + System.out.println("\t" + type + ": \t" + found.count + + "/" + (found.totalTime / 1000) + + "s" + " \t(" + rate + + " nodes/second)"); + } + findTime = currentTime; + } + } + + void logNodeAddition(LinkedHashMap tags, + String type) { + Integer count = stats.get(type); + if (count == null) { + count = 1; + } else { + count++; + } + stats.put(type, count); + long currentTime = System.currentTimeMillis(); + if (firstLogTime == 0) { + firstLogTime = currentTime; + logTime = currentTime; + } + if (currentTime - logTime > 1432) { + System.out.println( + new Date(currentTime) + ": Saving " + type + " " + count + " \t(" + (1000.0 * (float) count + / (float) (currentTime - firstLogTime)) + " " + type + "/second)"); + logTime = currentTime; + } + } + + void describeLoaded() { + logNodesFound(0); + for (String type : new String[]{"node", "way", "relation"}) { + Integer count = stats.get(type); + if (count != null) { + System.out.println("Loaded " + count + " " + type + "s"); + } + } + } + + protected abstract String getDatasetId(); + + private int missingNodeCount = 0; + + private void missingNode(long ndRef) { + if (missingNodeCount++ < 10) { + osmImporter.error("Cannot find node for osm-id " + ndRef); + } + } + + private void describeMissing() { + if (missingNodeCount > 0) { + osmImporter.error("When processing the ways, there were " + + missingNodeCount + " missing nodes"); + } + if (missingMemberCount > 0) { + osmImporter.error("When processing the relations, there were " + + missingMemberCount + " missing members"); + } + } + + private int missingMemberCount = 0; + + private void missingMember(String description) { + if (missingMemberCount++ < 10) { + osmImporter.error("Cannot find member: " + description); + } + } + + T currentNode = null; + T prev_way = null; + T prev_relation = null; + int nodeCount = 0; + int poiCount = 0; + int wayCount = 0; + int relationCount = 0; + int userCount = 0; + int changesetCount = 0; + + /** + * Add the BBox metadata to the dataset + */ + void addOSMBBox(Map bboxProperties) { + T bbox = addNode(LABEL_BBOX, bboxProperties, null); + createRelationship(osm_dataset, bbox, OSMRelation.BBOX); + } + + /** + * Create a new OSM node from the specified attributes (including + * location, user, changeset). The node is stored in the currentNode + * field, so that it can be used in the subsequent call to + * addOSMNodeTags after we close the XML tag for OSM nodes. + * + * @param nodeProps HashMap of attributes for the OSM-node + */ + void createOSMNode(Map nodeProps) { + T userNode = getUserNode(nodeProps); + T changesetNode = getChangesetNode(nodeProps, userNode); + currentNode = addNode(LABEL_NODE, nodeProps, PROP_NODE_ID); + createRelationship(currentNode, changesetNode, OSMRelation.CHANGESET); + nodeCount++; + } + + private void addOSMNodeTags(boolean allPoints, + LinkedHashMap currentNodeTags) { + currentNodeTags.remove("created_by"); // redundant information + // Nodes with tags get added to the index as point geometries + if (allPoints || currentNodeTags.size() > 0) { + Map nodeProps = getNodeProperties(currentNode); + double[] location = new double[]{ + (Double) nodeProps.get("lon"), + (Double) nodeProps.get("lat")}; + addNodeGeometry(currentNode, GTYPE_POINT, new Envelope(location), 1); + poiCount++; + } + addNodeTags(currentNode, currentNodeTags, "node"); + } + + protected void debugNodeWithId(T node, String idName, long[] idValues) { + Map nodeProperties = getNodeProperties(node); + String node_osm_id = nodeProperties.get(idName).toString(); + for (long idValue : idValues) { + if (node_osm_id.equals(Long.toString(idValue))) { + System.out.println("Debug node: " + node_osm_id); + } + } + } + + protected void createOSMWay(Map wayProperties, + ArrayList wayNodes, LinkedHashMap wayTags) { + RoadDirection direction = getRoadDirection(wayTags); + String name = (String) wayTags.get("name"); + int geometry = GTYPE_LINESTRING; + boolean isRoad = wayTags.containsKey("highway"); + if (isRoad) { + wayProperties.put("oneway", direction.toString()); + wayProperties.put("highway", wayTags.get("highway")); + } + if (name != null) { + // Copy name tag to way because this seems like a valuable + // location for + // such a property + wayProperties.put("name", name); + } + T userNode = getUserNode(wayProperties); + T changesetNode = getChangesetNode(wayProperties, userNode); + T way = addNode(LABEL_WAY, wayProperties, PROP_WAY_ID); + createRelationship(way, changesetNode, OSMRelation.CHANGESET); + if (prev_way == null) { + createRelationship(osm_dataset, way, OSMRelation.WAYS); + } else { + createRelationship(prev_way, way, OSMRelation.NEXT); + } + prev_way = way; + addNodeTags(way, wayTags, "way"); + Envelope bbox = null; + T firstNode = null; + T prevNode = null; + T prevProxy = null; + Map prevProps = null; + LinkedHashMap relProps = new LinkedHashMap<>(); + HashMap directionProps = new HashMap<>(); + directionProps.put("oneway", true); + for (long nd_ref : wayNodes) { + T pointNode = getOSMNode(nd_ref, changesetNode); + if (pointNode == null) { + /* + * This can happen if we import not whole planet, so some referenced + * nodes will be unavailable + */ + missingNode(nd_ref); + continue; + } + T proxyNode = createProxyNode(); + if (firstNode == null) { + firstNode = pointNode; + } + if (prevNode == pointNode) { + continue; + } + createRelationship(proxyNode, pointNode, OSMRelation.NODE, null); + Map nodeProps = getNodeProperties(pointNode); + double[] location = new double[]{ + (Double) nodeProps.get("lon"), + (Double) nodeProps.get("lat")}; + if (bbox == null) { + bbox = new Envelope(location); + } else { + bbox.expandToInclude(location); + } + if (prevProxy == null) { + createRelationship(way, proxyNode, OSMRelation.FIRST_NODE); + } else { + relProps.clear(); + double[] prevLoc = new double[]{(Double) prevProps.get("lon"), (Double) prevProps.get("lat")}; + double length = distance(prevLoc[0], prevLoc[1], location[0], location[1]); + relProps.put("length", length); + /* + * We default to bi-directional (and don't store direction in the way node), + * but if it is one-way we mark it as such, and define the direction using the relationship direction + */ + if (direction == RoadDirection.BACKWARD) { + createRelationship(proxyNode, prevProxy, OSMRelation.NEXT, relProps); + } else { + createRelationship(prevProxy, proxyNode, OSMRelation.NEXT, relProps); + } + } + prevNode = pointNode; + prevProxy = proxyNode; + prevProps = nodeProps; + } + if (firstNode != null && prevNode == firstNode) { + geometry = GTYPE_POLYGON; + } + if (wayNodes.size() < 2) { + geometry = GTYPE_POINT; + } + addNodeGeometry(way, geometry, bbox, wayNodes.size()); + this.wayCount++; + } + + private void createOSMRelation(Map relationProperties, + ArrayList> relationMembers, + LinkedHashMap relationTags) { + String name = (String) relationTags.get("name"); + if (name != null) { + /* Copy name tag to way because this seems like a valuable location for such a property */ + relationProperties.put("name", name); + } + T relation = addNode(LABEL_RELATION, relationProperties, PROP_RELATION_ID); + if (prev_relation == null) { + createRelationship(osm_dataset, relation, OSMRelation.RELATIONS); + } else { + createRelationship(prev_relation, relation, OSMRelation.NEXT); + } + prev_relation = relation; + addNodeTags(relation, relationTags, "relation"); + // We will test for cases that invalidate multilinestring further down + GeometryMetaData metaGeom = new GeometryMetaData(GTYPE_MULTILINESTRING); + T prevMember = null; + LinkedHashMap relProps = new LinkedHashMap(); + for (Map memberProps : relationMembers) { + String memberType = (String) memberProps.get("type"); + long member_ref = Long.parseLong(memberProps.get("ref").toString()); + if (memberType != null) { + T member = null; + switch (memberType) { + case "node": + member = getSingleNode(LABEL_NODE, memberType + "_osm_id", member_ref); + break; + case "way": + member = getSingleNode(LABEL_WAY, memberType + "_osm_id", member_ref); + break; + case "relation": + member = getSingleNode(LABEL_RELATION, memberType + "_osm_id", member_ref); + break; + } + if (null == member || prevMember == member) { + /* + * This can happen if we import not whole planet, so some + * referenced nodes will be unavailable + */ + missingMember(memberProps.toString()); + continue; + } + if (member == relation) { + osmImporter.error("Cannot add relation to same member: relation[" + relationTags + "] - member[" + + memberProps + "]"); + continue; + } + Map nodeProps = getNodeProperties(member); + if (memberType.equals("node")) { + double[] location = new double[]{(Double) nodeProps.get("lon"), (Double) nodeProps.get("lat")}; + metaGeom.expandToIncludePoint(location); + } else if (memberType.equals("nodes")) { + System.err.println("Unexpected 'nodes' member type"); + } else { + updateGeometryMetaDataFromMember(member, metaGeom, nodeProps); + } + relProps.clear(); + String role = (String) memberProps.get("role"); + if (role != null && role.length() > 0) { + relProps.put("role", role); + if (role.equals("outer")) { + metaGeom.setPolygon(); + } + } + createRelationship(relation, member, OSMRelation.MEMBER, relProps); + prevMember = member; + } else { + System.err.println("Cannot process invalid relation member: " + memberProps.toString()); + } + } + if (metaGeom.isValid()) { + addNodeGeometry(relation, metaGeom.getGeometryType(), + metaGeom.getBBox(), metaGeom.getVertices()); + } + this.relationCount++; + } + + /** + * This method should be overridden by implementation that are able to + * perform database or index optimizations when requested, like the + * batch inserter. + */ + protected abstract void optimize(); + + protected abstract T getSingleNode(Label label, String property, Object value); + + protected abstract Map getNodeProperties(T member); + + protected abstract T getOSMNode(long osmId, T changesetNode); + + protected abstract void updateGeometryMetaDataFromMember(T member, + GeometryMetaData metaGeom, Map nodeProps); + + protected abstract void finish(); + + protected abstract T createProxyNode(); + + protected abstract T getChangesetNode(Map nodeProps, T userNode); + + protected abstract T getUserNode(Map nodeProps); + + } + + private static final class WrappedNode { + + private Node inner; + + private WrappedNode(Node inner) { + this.inner = inner; + } + + static WrappedNode fromNode(Node node) { + return node == null ? null : new WrappedNode(node); + } + + void refresh(Transaction tx) { + String id = inner.getElementId(); + inner = tx.getNodeByElementId(id); + if (inner == null) { + throw new IllegalStateException("Failed to find node by id: " + id); + } + } + + Object getProperty(String key) { + return inner.getProperty(key); + } + + Object getProperty(String key, Object defaultValue) { + return inner.getProperty(key, defaultValue); + } + + void setProperty(String key, Object value) { + this.inner.setProperty(key, value); + } + + public String getId() { + return inner.getElementId(); + } + + public Relationship createRelationshipTo(WrappedNode usersNode, OSMRelation users) { + return inner.createRelationshipTo(usersNode.inner, users); + } + + public Relationship createRelationshipTo(Node usersNode, OSMRelation users) { + return inner.createRelationshipTo(usersNode, users); + } + + public Iterable getPropertyKeys() { + return inner.getPropertyKeys(); + } + + public ResourceIterable getRelationships(Direction direction, OSMRelation relType) { + return inner.getRelationships(direction, relType); + } + + public ResourceIterable getRelationships(OSMRelation geom) { + return inner.getRelationships(geom); + } + } + + private static class OSMGraphWriter extends OSMWriter { + + private final GraphDatabaseService graphDb; + private final SecurityContext securityContext; + private long currentChangesetId = -1; + private WrappedNode currentChangesetNode; + private long currentUserId = -1; + private WrappedNode currentUserNode; + private WrappedNode usersNode; + private final HashMap changesetNodes = new HashMap<>(); + private Transaction tx; + private int checkCount = 0; + private final int txInterval; + private IndexDefinition nodeIndex; + private IndexDefinition wayIndex; + private IndexDefinition relationIndex; + private IndexDefinition changesetIndex; + private IndexDefinition userIndex; + private final String layerHash; + private final HashMap hashedLabels = new HashMap<>(); + + private OSMGraphWriter(GraphDatabaseService graphDb, SecurityContext securityContext, StatsManager statsManager, + OSMImporter osmImporter, int txInterval) throws NoSuchAlgorithmException { + super(statsManager, osmImporter); + this.graphDb = graphDb; + this.securityContext = securityContext; + this.txInterval = txInterval; + if (this.txInterval < 100) { + System.err.println("Warning: Unusually short txInterval, expect bad insert performance"); + } + this.layerHash = md5Hash(osmImporter.layerName); + checkTx(null); // Opens transaction for future writes + } + + private static String md5Hash(String text) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(text.getBytes()); + byte[] digest = md.digest(); + String hashed = DatatypeConverter.printHexBinary(digest).toUpperCase(); + return hashed; + } + + private void successTx() { + if (tx != null) { + tx.commit(); + tx.close(); + tx = null; + checkCount = 0; + } + } + + private Transaction beginTx(GraphDatabaseService database) { + return beginTx(database, securityContext); + } + + private Transaction beginIndexTx(GraphDatabaseService database) { + return beginTx(database, IndexManager.IndexAccessMode.withIndexCreate(securityContext)); + } + + private static Transaction beginTx(GraphDatabaseService database, SecurityContext securityContext) { + if (!(database instanceof GraphDatabaseAPI)) { + throw new IllegalArgumentException("database must implement GraphDatabaseAPI"); + } + return ((GraphDatabaseAPI) database).beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext); + } + + private void beginTx() { + tx = beginTx(graphDb); + recoverNode(osm_dataset); + recoverNode(currentNode); + recoverNode(prev_relation); + recoverNode(prev_way); + recoverNode(currentChangesetNode); + recoverNode(currentUserNode); + recoverNode(usersNode); + changesetNodes.forEach((id, node) -> node.refresh(tx)); + } + + private WrappedNode checkTx(WrappedNode previous) { + if (checkCount++ > txInterval || tx == null || checkCount > 10) { + successTx(); + beginTx(); + recoverNode(previous); + } + return previous; + } + + private void recoverNode(WrappedNode outOfTx) { + if (outOfTx != null) { + outOfTx.refresh(tx); + } + } + + private WrappedNode findNodeByName(Label label, String name) { + Node node = findNodeByLabelProperty(tx, label, "name", name); + if (node != null) { + return WrappedNode.fromNode(node); + } + return null; + } + + private WrappedNode createNodeWithLabel(Transaction tx, Label label) { + Label hashed = getLabelHashed(label); + return WrappedNode.fromNode(tx.createNode(label, hashed)); + } + + @Override + protected void startWays() { + System.out.println("About to create node index"); + nodeIndex = createIndex(LABEL_NODE, PROP_NODE_ID); + System.out.println("About to populate node index"); + // TODO: Should we use another TX? + tx.schema().awaitIndexOnline(nodeIndex, 1, TimeUnit.MINUTES); // could be a large index + System.out.println("Finished populating node index"); + } + + @Override + protected void startRelations() { + System.out.println("About to create way and relation indexes"); + wayIndex = createIndex(LABEL_WAY, PROP_WAY_ID); + relationIndex = createIndex(LABEL_RELATION, PROP_RELATION_ID); + System.out.println("About to populate way and relation indexes"); + // TODO: Should we use another TX? + tx.schema().awaitIndexOnline(wayIndex, 1, TimeUnit.MINUTES); + tx.schema().awaitIndexOnline(nodeIndex, 1, TimeUnit.MINUTES); + System.out.println("Finished populating way and relation indexes"); + } + + protected void optimize() { + for (IndexDefinition index : new IndexDefinition[]{nodeIndex, wayIndex, relationIndex}) { + if (index != null) { + tx.schema().awaitIndexOnline(index, 30, TimeUnit.MINUTES); + } + } + } + + private Label getLabelHashed(Label label) { + if (hashedLabels.containsKey(label)) { + return hashedLabels.get(label); + } else { + Label hashed = Label.label(label.name() + "_" + layerHash); + hashedLabels.put(label, hashed); + return hashed; + } + } + + private Node findNodeByLabelProperty(Transaction tx, Label label, String propertyKey, Object value) { + Label hashed = getLabelHashed(label); + return tx.findNode(hashed, propertyKey, value); + } + + private IndexDefinition createIndex(Label label, String propertyKey) { + Label hashed = getLabelHashed(label); + String indexName = String.format("OSM-%s-%s-%s", osmImporter.layerName, hashed.name(), propertyKey); + IndexDefinition index = findIndex(tx, indexName, hashed, propertyKey); + if (index == null) { + successTx(); + try (Transaction indexTx = beginIndexTx(graphDb)) { + index = indexTx.schema().indexFor(hashed).on(propertyKey).withName(indexName).create(); + indexTx.commit(); + } + System.out.println("Created index " + index.getName()); + beginTx(); + } + return index; + } + + private IndexDefinition createIndexIfNotNull(IndexDefinition index, Label label, String propertyKey) { + if (index == null) { + index = createIndex(label, propertyKey); + tx.schema().awaitIndexOnline(index, 1, TimeUnit.MINUTES); // small index should be fast + } + return index; + } + + private IndexDefinition findIndex(Transaction tx, String indexName, Label label, String propertyKey) { + for (IndexDefinition index : tx.schema().getIndexes(label)) { + for (String prop : index.getPropertyKeys()) { + if (prop.equals(propertyKey)) { + if (index.getName().equals(indexName)) { + return index; + } else { + throw new IllegalStateException( + String.format("Found pre-existing index '%s' for index '%s'", index.getName(), + indexName)); + } + } + } + } + return null; + } + + private WrappedNode getOrCreateNode(Label label, String name, String type) { + WrappedNode node = findNodeByName(label, name); + if (node == null) { + WrappedNode n = createNodeWithLabel(tx, label); + n.setProperty("name", name); + n.setProperty("type", type); + node = checkTx(n); + } + return node; + } + + @Override + protected WrappedNode getOrCreateOSMDataset(String name) { + if (osm_dataset == null) { + osm_dataset = getOrCreateNode(LABEL_DATASET, name, "osm"); + } + return osm_dataset; + } + + @Override + protected void setDatasetProperties(Map extractProperties) { + for (String key : extractProperties.keySet()) { + osm_dataset.setProperty(key, extractProperties.get(key)); + } + } + + private void addProperties(Entity node, Map properties) { + for (String property : properties.keySet()) { + node.setProperty(property, properties.get(property)); + } + } + + @Override + protected void addNodeTags(WrappedNode node, LinkedHashMap tags, String type) { + logNodeAddition(tags, type); + if (node != null && tags.size() > 0) { + statsManager.addToTagStats(type, tags.keySet()); + WrappedNode tagsNode = createNodeWithLabel(tx, LABEL_TAGS); + addProperties(tagsNode.inner, tags); + node.createRelationshipTo(tagsNode, OSMRelation.TAGS); + tags.clear(); + } + } + + @Override + protected void addNodeGeometry(WrappedNode node, int gtype, Envelope bbox, int vertices) { + if (node != null && bbox != null && vertices > 0) { + if (gtype == GTYPE_GEOMETRY) { + gtype = vertices > 1 ? GTYPE_MULTIPOINT : GTYPE_POINT; + } + Node geomNode = tx.createNode(); + geomNode.setProperty("gtype", gtype); + geomNode.setProperty("vertices", vertices); + geomNode.setProperty(PROP_BBOX, + new double[]{bbox.getMinX(), bbox.getMaxX(), bbox.getMinY(), bbox.getMaxY()}); + node.createRelationshipTo(geomNode, OSMRelation.GEOM); + statsManager.addGeomStats(gtype); + } + } + + @Override + protected WrappedNode addNode(Label label, Map properties, String indexKey) { + WrappedNode node = createNodeWithLabel(tx, label); + if (indexKey != null && properties.containsKey(indexKey)) { + properties.put(indexKey, Long.parseLong(properties.get(indexKey).toString())); + } + addProperties(node.inner, properties); + return checkTx(node); + } + + @Override + protected void createRelationship(WrappedNode from, WrappedNode to, OSMRelation relType, + LinkedHashMap relProps) { + if (from != null & to != null) { + Relationship rel = from.createRelationshipTo(to, relType); + if (relProps != null && relProps.size() > 0) { + addProperties(rel, relProps); + } + } + } + + @Override + protected String getDatasetId() { + return osm_dataset.getId(); + } + + @Override + protected WrappedNode getSingleNode(Label label, String property, Object value) { + Node node = findNodeByLabelProperty(tx, LABEL_NODE, property, value); + return node == null ? null : WrappedNode.fromNode(node); + } + + @Override + protected Map getNodeProperties(WrappedNode node) { + LinkedHashMap properties = new LinkedHashMap<>(); + for (String property : node.getPropertyKeys()) { + properties.put(property, node.getProperty(property)); + } + return properties; + } + + @Override + protected WrappedNode getOSMNode(long osmId, WrappedNode changesetNode) { + if (currentChangesetNode != changesetNode || changesetNodes.isEmpty()) { + currentChangesetNode = changesetNode; + changesetNodes.clear(); + if (changesetNode != null) { + try (var relationships = changesetNode.getRelationships(Direction.INCOMING, + OSMRelation.CHANGESET)) { + for (Relationship rel : relationships) { + Node node = rel.getStartNode(); + Long nodeOsmId = (Long) node.getProperty(PROP_NODE_ID, null); + if (nodeOsmId != null) { + changesetNodes.put(nodeOsmId, WrappedNode.fromNode(node)); + } + } + } + } + } + WrappedNode node = changesetNodes.get(osmId); + if (node == null) { + logNodeFoundFrom("node-index"); + node = WrappedNode.fromNode(findNodeByLabelProperty(tx, LABEL_NODE, PROP_NODE_ID, osmId)); + } else { + logNodeFoundFrom(PROP_CHANGESET); + } + return node; + } + + @Override + protected void updateGeometryMetaDataFromMember(WrappedNode member, GeometryMetaData metaGeom, + Map nodeProps) { + try (var relationships = member.getRelationships(OSMRelation.GEOM)) { + for (Relationship rel : relationships) { + nodeProps = getNodeProperties(WrappedNode.fromNode(rel.getEndNode())); + metaGeom.checkSupportedGeometry((Integer) nodeProps.get("gtype")); + metaGeom.expandToIncludeBBox(nodeProps); + } + } + } + + @Override + protected void finish() { + if (tx == null) { + beginTx(); + } + osm_dataset.setProperty("relationCount", + (Integer) osm_dataset.getProperty("relationCount", 0) + relationCount); + osm_dataset.setProperty("wayCount", (Integer) osm_dataset.getProperty("wayCount", 0) + wayCount); + osm_dataset.setProperty("nodeCount", (Integer) osm_dataset.getProperty("nodeCount", 0) + nodeCount); + osm_dataset.setProperty("poiCount", (Integer) osm_dataset.getProperty("poiCount", 0) + poiCount); + osm_dataset.setProperty("changesetCount", + (Integer) osm_dataset.getProperty("changesetCount", 0) + changesetCount); + osm_dataset.setProperty("userCount", (Integer) osm_dataset.getProperty("userCount", 0) + userCount); + successTx(); + } + + @Override + protected WrappedNode createProxyNode() { + return WrappedNode.fromNode(tx.createNode(LABEL_WAY_NODE)); + } + + @Override + protected WrappedNode getChangesetNode(Map nodeProps, WrappedNode userNode) { + Object changesetObj = nodeProps.remove(PROP_CHANGESET); + if (changesetObj != null) { + long changeset = Long.parseLong(changesetObj.toString()); + if (changeset != currentChangesetId) { + changesetIndex = createIndexIfNotNull(changesetIndex, LABEL_CHANGESET, PROP_CHANGESET); + currentChangesetId = changeset; + Node changesetNode = findNodeByLabelProperty(tx, LABEL_CHANGESET, PROP_CHANGESET, + currentChangesetId); + if (changesetNode != null) { + currentChangesetNode = WrappedNode.fromNode(changesetNode); + } else { + LinkedHashMap changesetProps = new LinkedHashMap<>(); + changesetProps.put(PROP_CHANGESET, currentChangesetId); + changesetProps.put("timestamp", nodeProps.get("timestamp")); + currentChangesetNode = addNode(LABEL_CHANGESET, changesetProps, PROP_CHANGESET); + changesetCount++; + if (userNode != null) { + createRelationship(currentChangesetNode, userNode, OSMRelation.USER); + } + } + } + } else { + currentChangesetId = OSMWriter.UNKNOWN_CHANGESET; + currentChangesetNode = null; + missingChangesets++; + } + return currentChangesetNode; + } + + @Override + protected WrappedNode getUserNode(Map nodeProps) { + try { + long uid = Long.parseLong(nodeProps.remove(PROP_USER_ID).toString()); + String name = nodeProps.remove(PROP_USER_NAME).toString(); + if (uid != currentUserId) { + currentUserId = uid; + userIndex = createIndexIfNotNull(userIndex, LABEL_USER, PROP_USER_ID); + Node userNode = findNodeByLabelProperty(tx, LABEL_USER, PROP_USER_ID, currentUserId); + if (userNode != null) { + currentUserNode = WrappedNode.fromNode(userNode); + } else { + LinkedHashMap userProps = new LinkedHashMap<>(); + userProps.put(PROP_USER_ID, currentUserId); + userProps.put("name", name); + userProps.put("timestamp", nodeProps.get("timestamp")); + currentUserNode = addNode(LABEL_USER, userProps, PROP_USER_ID); + userCount++; + if (usersNode == null) { + usersNode = createNodeWithLabel(tx, LABEL_USER); + osm_dataset.createRelationshipTo(usersNode, OSMRelation.USERS); + } + usersNode.createRelationshipTo(currentUserNode, OSMRelation.OSM_USER); + } + } + } catch (Exception e) { + currentUserId = -1; + currentUserNode = null; + logMissingUser(nodeProps); + } + return currentUserNode; + } + + public String toString() { + return "OSMGraphWriter: DatabaseService[" + graphDb + "]:txInterval[" + this.txInterval + "]"; + } + + } + + public void importFile(GraphDatabaseService database, String dataset) throws Exception { + importFile(database, dataset, false, 5000); + } + + public void importFile(GraphDatabaseService database, String dataset, int txInterval) throws Exception { + importFile(database, dataset, false, txInterval); + } + + public void importFile(GraphDatabaseService database, String dataset, boolean allPoints, int txInterval) + throws Exception { + importFile(OSMWriter.fromGraphDatabase(database, securityContext, stats, this, txInterval), dataset, allPoints, + charset); + } + + public static class CountedFileReader extends InputStreamReader { + + private long length = 0; + private long charsRead = 0; + + public CountedFileReader(String path, Charset charset) throws FileNotFoundException { + super(new FileInputStream(path), charset); + this.length = (new File(path)).length(); + } + + public long getCharsRead() { + return charsRead; + } + + public long getlength() { + return length; + } + + public double getProgress() { + return length > 0 ? (double) charsRead / (double) length : 0; + } + + public int getPercentRead() { + return (int) (100.0 * getProgress()); + } + + public int read(char[] cbuf, int offset, int length) + throws IOException { + int read = super.read(cbuf, offset, length); + if (read > 0) { + charsRead += read; + } + return read; + } + } + + private int progress = 0; + private long progressTime = 0; + + private void beginProgressMonitor(int length) { + monitor.begin(length); + progress = 0; + progressTime = System.currentTimeMillis(); + } + + private void updateProgressMonitor(int currentProgress) { + if (currentProgress > this.progress) { + long time = System.currentTimeMillis(); + if (time - progressTime > 1000) { + monitor.worked(currentProgress - progress); + progress = currentProgress; + progressTime = time; + } + } + } + + private void endProgressMonitor() { + monitor.done(); + progress = 0; + progressTime = 0; + } + + public void setSecurityContext(SecurityContext securityContext) { + this.securityContext = securityContext; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + public void importFile(OSMWriter osmWriter, String dataset, boolean allPoints, Charset charset) + throws IOException, XMLStreamException { + log("Importing with osm-writer: " + osmWriter); + osmWriter.getOrCreateOSMDataset(layerName); + osm_dataset = osmWriter.getDatasetId(); + + long startTime = System.currentTimeMillis(); + long[] times = new long[]{0L, 0L, 0L, 0L}; + javax.xml.stream.XMLInputFactory factory = javax.xml.stream.XMLInputFactory.newInstance(); + CountedFileReader reader = new CountedFileReader(dataset, charset); + javax.xml.stream.XMLStreamReader parser = factory.createXMLStreamReader(reader); + int countXMLTags = 0; + beginProgressMonitor(100); + setLogContext(dataset); + boolean startedWays = false; + boolean startedRelations = false; + try { + ArrayList currentXMLTags = new ArrayList<>(); + int depth = 0; + Map wayProperties = null; + ArrayList wayNodes = new ArrayList<>(); + Map relationProperties = null; + ArrayList> relationMembers = new ArrayList<>(); + LinkedHashMap currentNodeTags = new LinkedHashMap<>(); + while (true) { + updateProgressMonitor(reader.getPercentRead()); + incrLogContext(); + int event = parser.next(); + if (event == javax.xml.stream.XMLStreamConstants.END_DOCUMENT) { + break; + } + switch (event) { + case javax.xml.stream.XMLStreamConstants.START_ELEMENT: + currentXMLTags.add(depth, parser.getLocalName()); + String tagPath = currentXMLTags.toString(); + if (tagPath.equals("[osm]")) { + osmWriter.setDatasetProperties(extractProperties(parser)); + } else if (tagPath.equals("[osm, bounds]")) { + osmWriter.addOSMBBox(extractProperties(PROP_BBOX, parser)); + } else if (tagPath.equals("[osm, node]")) { + /* */ + boolean includeNode = true; + Map nodeProperties = extractProperties("node", parser); + if (filterEnvelope != null) { + includeNode = filterEnvelope.contains((Double) nodeProperties.get("lon"), + (Double) nodeProperties.get("lat")); + } + if (includeNode) { + osmWriter.createOSMNode(nodeProperties); + } + } else if (tagPath.equals("[osm, way]")) { + /* */ + if (!startedWays) { + startedWays = true; + osmWriter.startWays(); + times[0] = System.currentTimeMillis(); + osmWriter.optimize(); + times[1] = System.currentTimeMillis(); + } + wayProperties = extractProperties("way", parser); + wayNodes.clear(); + } else if (tagPath.equals("[osm, way, nd]")) { + Map properties = extractProperties(parser); + wayNodes.add(Long.parseLong(properties.get("ref").toString())); + } else if (tagPath.endsWith("tag]")) { + Map properties = extractProperties(parser); + currentNodeTags.put(properties.get("k").toString(), + properties.get("v").toString()); + } else if (tagPath.equals("[osm, relation]")) { + /* */ + if (!startedRelations) { + startedRelations = true; + osmWriter.startRelations(); + times[2] = System.currentTimeMillis(); + osmWriter.optimize(); + times[3] = System.currentTimeMillis(); + } + relationProperties = extractProperties("relation", parser); + relationMembers.clear(); + } else if (tagPath.equals("[osm, relation, member]")) { + relationMembers.add(extractProperties(parser)); + } + if (startedRelations) { + if (countXMLTags < 10) { + debug("Starting tag at depth " + depth + ": " + + currentXMLTags.get(depth) + " - " + + currentXMLTags.toString()); + for (int i = 0; i < parser.getAttributeCount(); i++) { + debug("\t" + currentXMLTags.toString() + ": " + + parser.getAttributeLocalName(i) + "[" + + parser.getAttributeNamespace(i) + "," + + parser.getAttributePrefix(i) + "," + + parser.getAttributeType(i) + "," + + "] = " + parser.getAttributeValue(i)); + } + } + countXMLTags++; + } + depth++; + break; + case javax.xml.stream.XMLStreamConstants.END_ELEMENT: + switch (currentXMLTags.toString()) { + case "[osm, node]": + osmWriter.addOSMNodeTags(allPoints, currentNodeTags); + break; + case "[osm, way]": + osmWriter.createOSMWay(wayProperties, wayNodes, currentNodeTags); + break; + case "[osm, relation]": + osmWriter.createOSMRelation(relationProperties, relationMembers, currentNodeTags); + break; + } + depth--; + currentXMLTags.remove(depth); + break; + default: + break; + } + } + } finally { + endProgressMonitor(); + parser.close(); + osmWriter.finish(); + this.osm_dataset = osmWriter.getDatasetId(); + this.missingChangesets = osmWriter.missingChangesets; + } + if (verboseLog) { + describeTimes(startTime, times); + osmWriter.describeMissing(); + osmWriter.describeLoaded(); + + long stopTime = System.currentTimeMillis(); + log("info | Elapsed time in seconds: " + (1.0 * (stopTime - startTime) / 1000.0)); + stats.dumpGeomStats(); + stats.printTagStats(); + } + } + + private void describeTimes(long startTime, long[] times) { + long endTime = System.currentTimeMillis(); + log("Completed load in " + (1.0 * (endTime - startTime) / 1000.0) + "s"); + log("\tImported nodes: " + (1.0 * (times[0] - startTime) / 1000.0) + "s"); + log("\tOptimized index: " + (1.0 * (times[1] - times[0]) / 1000.0) + "s"); + log("\tImported ways: " + (1.0 * (times[2] - times[1]) / 1000.0) + "s"); + log("\tOptimized index: " + (1.0 * (times[3] - times[2]) / 1000.0) + "s"); + log("\tImported rels: " + (1.0 * (endTime - times[3]) / 1000.0) + "s"); + } + + private Map extractProperties(XMLStreamReader parser) { + return extractProperties(null, parser); + } + + private Map extractProperties(String name, XMLStreamReader parser) { /* */ - LinkedHashMap properties = new LinkedHashMap(); - for (int i = 0; i < parser.getAttributeCount(); i++) { - String prop = parser.getAttributeLocalName(i); - String value = parser.getAttributeValue(i); - if (name != null && prop.equals("id")) { - prop = name + "_osm_id"; - name = null; - } - if (prop.equals("lat") || prop.equals("lon")) { - properties.put(prop, Double.parseDouble(value)); - } else if (name != null && prop.equals("version")) { - properties.put(prop, Integer.parseInt(value)); - } else if (prop.equals("visible")) { - if (!value.equals("true") && !value.equals("1")) { - properties.put(prop, false); - } - } else if (prop.equals("timestamp")) { - try { - Date timestamp = timestampFormat.parse(value); - properties.put(prop, timestamp.getTime()); - } catch (ParseException e) { - error("Error parsing timestamp", e); - } - } else { - properties.put(prop, value); - } - } - if (name != null) { - properties.put("name", name); - } - return properties; - } - - /** - * Retrieves the direction of the given road, i.e. whether it is a one-way road from its start node, - * a one-way road to its start node or a two-way road. - * - * @param wayProperties the property map of the road - * @return BOTH if it's a two-way road, FORWARD if it's a one-way road from the start node, - * or BACKWARD if it's a one-way road to the start node - */ - public static RoadDirection getRoadDirection(Map wayProperties) { - String oneway = (String) wayProperties.get("oneway"); - if (null != oneway) { - if ("-1".equals(oneway)) return RoadDirection.BACKWARD; - if ("1".equals(oneway) || "yes".equalsIgnoreCase(oneway) || "true".equalsIgnoreCase(oneway)) - return RoadDirection.FORWARD; - } - return RoadDirection.BOTH; - } - - /** - * Calculate correct distance between 2 points on Earth. - * - * @param latA - * @param lonA - * @param latB - * @param lonB - * @return distance in meters - */ - public static double distance(double lonA, double latA, double lonB, double latB) { - return WGS84.orthodromicDistance(lonA, latA, lonB, latB); - } - - private void log(PrintStream out, String message) { - if (logContext != null) { - message = logContext + "[" + contextLine + "]: " + message; - } - out.println(message); - } - - private void log(String message) { - if (verboseLog) { - log(System.out, message); - } - } - - private void debug(String message) { - if (debugLog) { - log(System.out, message); - } - } - - private void error(String message) { - log(System.err, message); - } - - private void error(String message, Exception e) { - log(System.err, message); - e.printStackTrace(System.err); - } - - private String logContext = null; - private int contextLine = 0; - private boolean debugLog = false; - private boolean verboseLog = true; - - // "2008-06-11T12:36:28Z" - private DateFormat timestampFormat = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss'Z'"); - - public void setDebug(boolean verbose) { - this.debugLog = verbose; - this.verboseLog |= verbose; - } - - public void setVerbose(boolean verbose) { - this.verboseLog = verbose; - this.debugLog &= verbose; - } - - private void setLogContext(String context) { - logContext = context; - contextLine = 0; - } - - private void incrLogContext() { - contextLine++; - } - - /** - * This method allows for a console, command-line application for loading - * one or more *.osm files into a new database. - * - * @param args , the database directory followed by one or more osm files - */ - public static void main(String[] args) { - if (args.length < 2) { - System.out.println("Usage: osmimporter databasedir osmfile <..osmfiles..>"); - } else { - OSMImportManager importer = new OSMImportManager(args[0]); - for (int i = 1; i < args.length; i++) { - try { - importer.loadTestOsmData(args[i], 5000); - } catch (Exception e) { - System.err.println("Error importing OSM file '" + args[i] + "': " + e); - e.printStackTrace(); - } finally { - importer.shutdown(); - } - } - } - } - - private static class OSMImportManager { - private DatabaseManagementService databases; - private GraphDatabaseService graphDb; - private File dbPath; - private String databaseName = "neo4j"; // can only be something other than neo4j in enterprise edition - - public OSMImportManager(String path) { - setDbPath(path); - } - - public void setDbPath(String path) { - dbPath = new File(path); - if (dbPath.exists()) { - if (!dbPath.isDirectory()) { - throw new RuntimeException("Database path is an existing file: " + dbPath.getAbsolutePath()); - } - } else { - dbPath.mkdirs(); - } - } - - private void loadTestOsmData(String layerName, int commitInterval) throws Exception { - String osmPath = layerName; - System.out.println("\n=== Loading layer " + layerName + " from " + osmPath + " ===\n"); - long start = System.currentTimeMillis(); - OSMImporter importer = new OSMImporter(layerName); - prepareDatabase(true); - importer.importFile(graphDb, osmPath, false, commitInterval); - importer.reIndex(graphDb, commitInterval); - shutdown(); - System.out.println("=== Completed loading " + layerName + " in " + (System.currentTimeMillis() - start) / 1000.0 + " seconds ==="); - } - - private DatabaseLayout prepareLayout(boolean delete) throws IOException { - Neo4jLayout homeLayout = Neo4jLayout.of(dbPath.toPath()); - DatabaseLayout databaseLayout = homeLayout.databaseLayout(databaseName); - if (delete) { - FileUtils.deleteDirectory(databaseLayout.databaseDirectory()); - FileUtils.deleteDirectory(databaseLayout.getTransactionLogsDirectory()); - } - return databaseLayout; - } - - private void prepareDatabase(boolean delete) throws IOException { - shutdown(); - prepareLayout(delete); - databases = new DatabaseManagementServiceBuilder(dbPath.toPath()).build(); - graphDb = databases.database(databaseName); - } - - protected void shutdown() { - if (databases != null) { - databases.shutdown(); - databases = null; - graphDb = null; - } - } - } + LinkedHashMap properties = new LinkedHashMap(); + for (int i = 0; i < parser.getAttributeCount(); i++) { + String prop = parser.getAttributeLocalName(i); + String value = parser.getAttributeValue(i); + if (name != null && prop.equals("id")) { + prop = name + "_osm_id"; + name = null; + } + if (prop.equals("lat") || prop.equals("lon")) { + properties.put(prop, Double.parseDouble(value)); + } else if (name != null && prop.equals("version")) { + properties.put(prop, Integer.parseInt(value)); + } else if (prop.equals("visible")) { + if (!value.equals("true") && !value.equals("1")) { + properties.put(prop, false); + } + } else if (prop.equals("timestamp")) { + try { + Date timestamp = timestampFormat.parse(value); + properties.put(prop, timestamp.getTime()); + } catch (ParseException e) { + error("Error parsing timestamp", e); + } + } else { + properties.put(prop, value); + } + } + if (name != null) { + properties.put("name", name); + } + return properties; + } + + /** + * Retrieves the direction of the given road, i.e. whether it is a one-way road from its start node, + * a one-way road to its start node or a two-way road. + * + * @param wayProperties the property map of the road + * @return BOTH if it's a two-way road, FORWARD if it's a one-way road from the start node, + * or BACKWARD if it's a one-way road to the start node + */ + public static RoadDirection getRoadDirection(Map wayProperties) { + String oneway = (String) wayProperties.get("oneway"); + if (null != oneway) { + if ("-1".equals(oneway)) { + return RoadDirection.BACKWARD; + } + if ("1".equals(oneway) || "yes".equalsIgnoreCase(oneway) || "true".equalsIgnoreCase(oneway)) { + return RoadDirection.FORWARD; + } + } + return RoadDirection.BOTH; + } + + /** + * Calculate correct distance between 2 points on Earth. + * + * @param latA + * @param lonA + * @param latB + * @param lonB + * @return distance in meters + */ + public static double distance(double lonA, double latA, double lonB, double latB) { + return WGS84.orthodromicDistance(lonA, latA, lonB, latB); + } + + private void log(PrintStream out, String message) { + if (logContext != null) { + message = logContext + "[" + contextLine + "]: " + message; + } + out.println(message); + } + + private void log(String message) { + if (verboseLog) { + log(System.out, message); + } + } + + private void debug(String message) { + if (debugLog) { + log(System.out, message); + } + } + + private void error(String message) { + log(System.err, message); + } + + private void error(String message, Exception e) { + log(System.err, message); + e.printStackTrace(System.err); + } + + private String logContext = null; + private int contextLine = 0; + private boolean debugLog = false; + private boolean verboseLog = true; + + // "2008-06-11T12:36:28Z" + private DateFormat timestampFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss'Z'"); + + public void setDebug(boolean verbose) { + this.debugLog = verbose; + this.verboseLog |= verbose; + } + + public void setVerbose(boolean verbose) { + this.verboseLog = verbose; + this.debugLog &= verbose; + } + + private void setLogContext(String context) { + logContext = context; + contextLine = 0; + } + + private void incrLogContext() { + contextLine++; + } + + /** + * This method allows for a console, command-line application for loading + * one or more *.osm files into a new database. + * + * @param args , the database directory followed by one or more osm files + */ + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Usage: osmimporter databasedir osmfile <..osmfiles..>"); + } else { + OSMImportManager importer = new OSMImportManager(args[0]); + for (int i = 1; i < args.length; i++) { + try { + importer.loadTestOsmData(args[i], 5000); + } catch (Exception e) { + System.err.println("Error importing OSM file '" + args[i] + "': " + e); + e.printStackTrace(); + } finally { + importer.shutdown(); + } + } + } + } + + private static class OSMImportManager { + + private DatabaseManagementService databases; + private GraphDatabaseService graphDb; + private File dbPath; + private String databaseName = "neo4j"; // can only be something other than neo4j in enterprise edition + + public OSMImportManager(String path) { + setDbPath(path); + } + + public void setDbPath(String path) { + dbPath = new File(path); + if (dbPath.exists()) { + if (!dbPath.isDirectory()) { + throw new RuntimeException("Database path is an existing file: " + dbPath.getAbsolutePath()); + } + } else { + dbPath.mkdirs(); + } + } + + private void loadTestOsmData(String layerName, int commitInterval) throws Exception { + String osmPath = layerName; + System.out.println("\n=== Loading layer " + layerName + " from " + osmPath + " ===\n"); + long start = System.currentTimeMillis(); + OSMImporter importer = new OSMImporter(layerName); + prepareDatabase(true); + importer.importFile(graphDb, osmPath, false, commitInterval); + importer.reIndex(graphDb, commitInterval); + shutdown(); + System.out.println( + "=== Completed loading " + layerName + " in " + (System.currentTimeMillis() - start) / 1000.0 + + " seconds ==="); + } + + private DatabaseLayout prepareLayout(boolean delete) throws IOException { + Neo4jLayout homeLayout = Neo4jLayout.of(dbPath.toPath()); + DatabaseLayout databaseLayout = homeLayout.databaseLayout(databaseName); + if (delete) { + FileUtils.deleteDirectory(databaseLayout.databaseDirectory()); + FileUtils.deleteDirectory(databaseLayout.getTransactionLogsDirectory()); + } + return databaseLayout; + } + + private void prepareDatabase(boolean delete) throws IOException { + shutdown(); + prepareLayout(delete); + databases = new DatabaseManagementServiceBuilder(dbPath.toPath()).build(); + graphDb = databases.database(databaseName); + } + + protected void shutdown() { + if (databases != null) { + databases.shutdown(); + databases = null; + graphDb = null; + } + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMLayer.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMLayer.java index b9e160f6c..6cfefceab 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMLayer.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMLayer.java @@ -21,13 +21,19 @@ import java.io.File; import java.util.HashMap; - +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.json.simple.JSONObject; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.Constants; +import org.neo4j.gis.spatial.DynamicLayer; +import org.neo4j.gis.spatial.DynamicLayerConfig; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.SpatialDataset; import org.neo4j.gis.spatial.rtree.NullListener; -import org.neo4j.graphdb.*; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Transaction; /** * Instances of this class represent the primary layer of the OSM Dataset. It @@ -35,230 +41,237 @@ * Only one is primary, the layer containing all ways. Other layers are dynamic. */ public class OSMLayer extends DynamicLayer { - private OSMDataset osmDataset; - @Override - public SpatialDataset getDataset() { - return osmDataset; - } + private OSMDataset osmDataset; + + @Override + public SpatialDataset getDataset() { + return osmDataset; + } - public void setDataset(OSMDataset osmDataset) { - this.osmDataset = osmDataset; - } + public void setDataset(OSMDataset osmDataset) { + this.osmDataset = osmDataset; + } - public Integer getGeometryType() { - // The core layer in OSM is based on the Ways, and we return all of them - // as LINESTRING and POLYGON, so we use the parent GEOMETRY - return GTYPE_GEOMETRY; - } + public Integer getGeometryType() { + // The core layer in OSM is based on the Ways, and we return all of them + // as LINESTRING and POLYGON, so we use the parent GEOMETRY + return GTYPE_GEOMETRY; + } - /** - * OSM always uses WGS84 CRS; so we return that. - * - * @param tx - */ - public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { - try { - return DefaultGeographicCRS.WGS84; - } catch (Exception e) { - System.err.println("Failed to decode WGS84 CRS: " + e.getMessage()); - e.printStackTrace(System.err); - return null; - } - } + /** + * OSM always uses WGS84 CRS; so we return that. + * + * @param tx + */ + public CoordinateReferenceSystem getCoordinateReferenceSystem(Transaction tx) { + try { + return DefaultGeographicCRS.WGS84; + } catch (Exception e) { + System.err.println("Failed to decode WGS84 CRS: " + e.getMessage()); + e.printStackTrace(System.err); + return null; + } + } - protected void clear(Transaction tx) { - indexWriter.clear(tx, new NullListener()); - } + protected void clear(Transaction tx) { + indexWriter.clear(tx, new NullListener()); + } - public Node addWay(Transaction tx, Node way) { - return addWay(tx, way, false); - } + public Node addWay(Transaction tx, Node way) { + return addWay(tx, way, false); + } - public Node addWay(Transaction tx, Node way, boolean verifyGeom) { - Relationship geomRel = way.getSingleRelationship(OSMRelation.GEOM, Direction.OUTGOING); - if (geomRel != null) { - Node geomNode = geomRel.getEndNode(); - try { - // This is a test of the validity of the geometry, throws exception on error - if (verifyGeom) - getGeometryEncoder().decodeGeometry(geomNode); - indexWriter.add(tx, geomNode); - } catch (Exception e) { - System.err.println("Failed geometry test on node " + geomNode.getProperty("name", geomNode.toString()) + ": " - + e.getMessage()); - for (String key : geomNode.getPropertyKeys()) { - System.err.println("\t" + key + ": " + geomNode.getProperty(key)); - } - System.err.println("For way node " + way); - for (String key : way.getPropertyKeys()) { - System.err.println("\t" + key + ": " + way.getProperty(key)); - } - // e.printStackTrace(System.err); - } - return geomNode; - } else { - return null; - } - } + public Node addWay(Transaction tx, Node way, boolean verifyGeom) { + Relationship geomRel = way.getSingleRelationship(OSMRelation.GEOM, Direction.OUTGOING); + if (geomRel != null) { + Node geomNode = geomRel.getEndNode(); + try { + // This is a test of the validity of the geometry, throws exception on error + if (verifyGeom) { + getGeometryEncoder().decodeGeometry(geomNode); + } + indexWriter.add(tx, geomNode); + } catch (Exception e) { + System.err.println( + "Failed geometry test on node " + geomNode.getProperty("name", geomNode.toString()) + ": " + + e.getMessage()); + for (String key : geomNode.getPropertyKeys()) { + System.err.println("\t" + key + ": " + geomNode.getProperty(key)); + } + System.err.println("For way node " + way); + for (String key : way.getPropertyKeys()) { + System.err.println("\t" + key + ": " + way.getProperty(key)); + } + // e.printStackTrace(System.err); + } + return geomNode; + } else { + return null; + } + } - /** - * Provides a method for iterating over all nodes that represent geometries in this layer. - * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return - * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to - * generate a Geometry. There is no restriction on a node belonging to multiple datasets, or - * multiple layers within the same dataset. - * - * @param tx - * @return iterable over geometry nodes in the dataset - */ - public Iterable getAllGeometryNodes(Transaction tx) { - return indexReader.getAllIndexedNodes(tx); - } + /** + * Provides a method for iterating over all nodes that represent geometries in this layer. + * This is similar to the getAllNodes() methods from GraphDatabaseService but will only return + * nodes that this dataset considers its own, and can be passed to the GeometryEncoder to + * generate a Geometry. There is no restriction on a node belonging to multiple datasets, or + * multiple layers within the same dataset. + * + * @param tx + * @return iterable over geometry nodes in the dataset + */ + public Iterable getAllGeometryNodes(Transaction tx) { + return indexReader.getAllIndexedNodes(tx); + } - public boolean removeDynamicLayer(Transaction tx, String name) { - return removeLayerConfig(tx, name); - } + public boolean removeDynamicLayer(Transaction tx, String name) { + return removeLayerConfig(tx, name); + } - /** - *

-     * { "step": {"type": "GEOM", "direction": "INCOMING"
-     *     "step": {"type": "TAGS", "direction": "OUTGOING"
-     *       "properties": {"highway": "residential"}
-     *     }
-     *   }
-     * }
-     * 
- *

- * This will work with OSM datasets, traversing from the geometry node - * to the way node and then to the tags node to test if the way is a - * residential street. - */ - @SuppressWarnings("unchecked") - public DynamicLayerConfig addDynamicLayerOnWayTags(Transaction tx, String name, int type, HashMap tags) { - JSONObject query = new JSONObject(); - if (tags != null && !tags.isEmpty()) { - JSONObject step2tags = new JSONObject(); - JSONObject step2way = new JSONObject(); - JSONObject properties = new JSONObject(); - for (Object key : tags.keySet()) { - Object value = tags.get(key); - if (value != null && (value.toString().length() < 1 || value.equals("*"))) - value = null; - properties.put(key.toString(), value); - } + /** + *

+	 * { "step": {"type": "GEOM", "direction": "INCOMING"
+	 *     "step": {"type": "TAGS", "direction": "OUTGOING"
+	 *       "properties": {"highway": "residential"}
+	 *     }
+	 *   }
+	 * }
+	 * 
+ *

+ * This will work with OSM datasets, traversing from the geometry node + * to the way node and then to the tags node to test if the way is a + * residential street. + */ + @SuppressWarnings("unchecked") + public DynamicLayerConfig addDynamicLayerOnWayTags(Transaction tx, String name, int type, HashMap tags) { + JSONObject query = new JSONObject(); + if (tags != null && !tags.isEmpty()) { + JSONObject step2tags = new JSONObject(); + JSONObject step2way = new JSONObject(); + JSONObject properties = new JSONObject(); + for (Object key : tags.keySet()) { + Object value = tags.get(key); + if (value != null && (value.toString().length() < 1 || value.equals("*"))) { + value = null; + } + properties.put(key.toString(), value); + } - step2tags.put("properties", properties); - step2tags.put("type", "TAGS"); - step2tags.put("direction", "OUTGOING"); + step2tags.put("properties", properties); + step2tags.put("type", "TAGS"); + step2tags.put("direction", "OUTGOING"); - step2way.put("step", step2tags); - step2way.put("type", "GEOM"); - step2way.put("direction", "INCOMING"); + step2way.put("step", step2tags); + step2way.put("type", "GEOM"); + step2way.put("direction", "INCOMING"); - query.put("step", step2way); - } - if (type > 0) { - JSONObject properties = new JSONObject(); - properties.put(PROP_TYPE, type); - query.put("properties", properties); - } - System.out.println("Created dynamic layer query: " + query.toJSONString()); - return addLayerConfig(tx, name, type, query.toJSONString()); - } + query.put("step", step2way); + } + if (type > 0) { + JSONObject properties = new JSONObject(); + properties.put(PROP_TYPE, type); + query.put("properties", properties); + } + System.out.println("Created dynamic layer query: " + query.toJSONString()); + return addLayerConfig(tx, name, type, query.toJSONString()); + } - /** - * Add a rule for a pure way based search, with a single property key/value - * match on the way tags. All ways with the specified tag property will be - * returned. This convenience method will automatically name the layer based - * on the key/value passed, namely 'key-value'. If you want more control - * over the naming, revert to the addDynamicLayerOnWayTags method. - * The geometry is assumed to be LineString, the most common type for ways. - * - * @param key key to match on way tags - * @param value value to match on way tags - */ - public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String key, String value) { - return addSimpleDynamicLayer(tx, key, value, Constants.GTYPE_LINESTRING); - } + /** + * Add a rule for a pure way based search, with a single property key/value + * match on the way tags. All ways with the specified tag property will be + * returned. This convenience method will automatically name the layer based + * on the key/value passed, namely 'key-value'. If you want more control + * over the naming, revert to the addDynamicLayerOnWayTags method. + * The geometry is assumed to be LineString, the most common type for ways. + * + * @param key key to match on way tags + * @param value value to match on way tags + */ + public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String key, String value) { + return addSimpleDynamicLayer(tx, key, value, Constants.GTYPE_LINESTRING); + } - /** - * Add a rule for a pure way based search, with a single property key/value - * match on the way tags. All ways with the specified tag property will be - * returned. This convenience method will automatically name the layer based - * on the key/value passed, namely 'key-value'. If you want more control - * over the naming, revert to the addDynamicLayerOnWayTags method. - * - * @param key key to match on way tags - * @param value value to match on way tags - * @param gtype type as defined in Constants. - */ - public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String key, String value, int gtype) { - HashMap tags = new HashMap<>(); - tags.put(key, value); - return addDynamicLayerOnWayTags(tx, value == null ? key : key + "-" + value, gtype, tags); - } + /** + * Add a rule for a pure way based search, with a single property key/value + * match on the way tags. All ways with the specified tag property will be + * returned. This convenience method will automatically name the layer based + * on the key/value passed, namely 'key-value'. If you want more control + * over the naming, revert to the addDynamicLayerOnWayTags method. + * + * @param key key to match on way tags + * @param value value to match on way tags + * @param gtype type as defined in Constants. + */ + public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String key, String value, int gtype) { + HashMap tags = new HashMap<>(); + tags.put(key, value); + return addDynamicLayerOnWayTags(tx, value == null ? key : key + "-" + value, gtype, tags); + } - /** - * Add a rule for a pure way based search, with multiple property key/value - * match on the way tags. All ways with the specified tag properties will be - * returned. This convenience method will automatically name the layer based - * on the key/value pairs passed, namely 'key-value-key-value-...'. If you - * want more control over the naming, revert to the addDynamicLayerOnWayTags - * method. - * - * @param gtype type as defined in Constants. - * @param tagsQuery String of ',' separated key=value tags to match - */ - public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, int gtype, String tagsQuery) { - HashMap tags = new HashMap<>(); - StringBuilder name = new StringBuilder(); - for (String query : tagsQuery.split("\\s*,\\s*")) { - String[] fields = query.split("\\s*=+\\s*"); - String key = fields[0]; - String value = fields.length > 1 ? fields[1] : "*"; - tags.put(key, value); - if (name.length() > 0) - name.append("-"); - name.append(key); - name.append("-"); - if (!value.equals("*")) - name.append(value); - } - return addDynamicLayerOnWayTags(tx, name.toString(), gtype, tags); - } + /** + * Add a rule for a pure way based search, with multiple property key/value + * match on the way tags. All ways with the specified tag properties will be + * returned. This convenience method will automatically name the layer based + * on the key/value pairs passed, namely 'key-value-key-value-...'. If you + * want more control over the naming, revert to the addDynamicLayerOnWayTags + * method. + * + * @param gtype type as defined in Constants. + * @param tagsQuery String of ',' separated key=value tags to match + */ + public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, int gtype, String tagsQuery) { + HashMap tags = new HashMap<>(); + StringBuilder name = new StringBuilder(); + for (String query : tagsQuery.split("\\s*,\\s*")) { + String[] fields = query.split("\\s*=+\\s*"); + String key = fields[0]; + String value = fields.length > 1 ? fields[1] : "*"; + tags.put(key, value); + if (name.length() > 0) { + name.append("-"); + } + name.append(key); + name.append("-"); + if (!value.equals("*")) { + name.append(value); + } + } + return addDynamicLayerOnWayTags(tx, name.toString(), gtype, tags); + } - /** - * Add a rule for a pure way based search, with multiple property key/value - * match on the way tags. All ways with the specified tag properties will be - * returned. This convenience method will automatically name the layer based - * on the key/value pairs passed, namely 'key-value-key-value-...'. If you - * want more control over the naming, revert to the addDynamicLayerOnWayTags - * method. The geometry type will be assumed to be LineString. - * - * @param tagsQuery String of ',' separated key=value tags to match - */ - public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String tagsQuery) { - return addSimpleDynamicLayer(tx, GTYPE_LINESTRING, tagsQuery); - } + /** + * Add a rule for a pure way based search, with multiple property key/value + * match on the way tags. All ways with the specified tag properties will be + * returned. This convenience method will automatically name the layer based + * on the key/value pairs passed, namely 'key-value-key-value-...'. If you + * want more control over the naming, revert to the addDynamicLayerOnWayTags + * method. The geometry type will be assumed to be LineString. + * + * @param tagsQuery String of ',' separated key=value tags to match + */ + public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, String tagsQuery) { + return addSimpleDynamicLayer(tx, GTYPE_LINESTRING, tagsQuery); + } - /** - * Add a rule for a pure way based search, with a check on geometry type only. - * - * @param gtype type as defined in Constants. - */ - public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, int gtype) { - return addDynamicLayerOnWayTags(tx, SpatialDatabaseService.convertGeometryTypeToName(gtype), gtype, null); - } + /** + * Add a rule for a pure way based search, with a check on geometry type only. + * + * @param gtype type as defined in Constants. + */ + public DynamicLayerConfig addSimpleDynamicLayer(Transaction tx, int gtype) { + return addDynamicLayerOnWayTags(tx, SpatialDatabaseService.convertGeometryTypeToName(gtype), gtype, null); + } - /** - * The OSM dataset has a number of possible stylesOverride this method to provide a style if your layer wishes to control - * its own rendering in the GIS. - * - * @return Style or null - */ - public File getStyle() { - // TODO: Replace with a proper resource lookup, since this will be in the JAR - return new File("dev/neo4j/neo4j-spatial/src/main/resources/sld/osm/osm.sld"); - } + /** + * The OSM dataset has a number of possible stylesOverride this method to provide a style if your layer wishes to + * control + * its own rendering in the GIS. + * + * @return Style or null + */ + public File getStyle() { + // TODO: Replace with a proper resource lookup, since this will be in the JAR + return new File("dev/neo4j/neo4j-spatial/src/main/resources/sld/osm/osm.sld"); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMLayerToShapefileExporter.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMLayerToShapefileExporter.java index 0b73a6b95..b8157cb89 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMLayerToShapefileExporter.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMLayerToShapefileExporter.java @@ -19,6 +19,12 @@ */ package org.neo4j.gis.spatial.osm; +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.dbms.api.DatabaseManagementServiceBuilder; import org.neo4j.gis.spatial.Constants; @@ -30,74 +36,70 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.File; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - public class OSMLayerToShapefileExporter { - /** - * This method allows for a console, command-line application for loading - * accessing an existing database containing an existing OSM model, and - * exporting one or more dynamic layers to shapefiles. The layer - * specifications are key.value pairs (dot separated). If the value is left - * out, all values are accepted (test for existance of key only). - * - * @param args , the database directory, OSM dataset and layer - * specifications. - */ - public static void main(String[] args) { - if (args.length < 5) { - System.out.println("Usage: osmtoshp neo4jHome database exportdir osmdataset layerspec <..layerspecs..>"); - System.out.println("\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); - } else { - String homeDir = args[0]; - String database = args[1]; - String exportDir = args[2]; - String osmdataset = args[3]; - List layerspecs = new ArrayList<>(Arrays.asList(args).subList(4, args.length)); - DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(homeDir)).build(); - GraphDatabaseService db = databases.database(database); - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); - OSMLayer layer; - try (Transaction tx = db.beginTx()) { - layer = (OSMLayer) spatial.getLayer(tx, osmdataset); - } - if (layer != null) { - ShapefileExporter exporter = new ShapefileExporter(db); - exporter.setExportDir(args[1] + File.separator + layer.getName()); - for (String layerspec : layerspecs) { - String[] fields = layerspec.split("[.\\-]"); - HashMap tags = new HashMap<>(); - String key = fields[0]; - String name = key; - if (fields.length > 1) { - String value = fields[1]; - name = key + "-" + value; - tags.put(key, value); - } - try (Transaction tx = db.beginTx()) { - if (layer.getLayerNames(tx).contains(name)) { - System.out.println("Exporting previously existing layer: " + name); - } else { - System.out.println("Creating and exporting new layer: " + name); - layer.addDynamicLayerOnWayTags(tx, name, Constants.GTYPE_LINESTRING, tags); - } - exporter.exportLayer(name); - tx.commit(); - } catch (Exception e) { - System.err.println("Failed to export dynamic layer " + name + ": " + e); - e.printStackTrace(System.err); - } - } - } else { - System.err.println("No such layer: " + args[2]); - } - databases.shutdown(); - } - } + /** + * This method allows for a console, command-line application for loading + * accessing an existing database containing an existing OSM model, and + * exporting one or more dynamic layers to shapefiles. The layer + * specifications are key.value pairs (dot separated). If the value is left + * out, all values are accepted (test for existance of key only). + * + * @param args , the database directory, OSM dataset and layer + * specifications. + */ + public static void main(String[] args) { + if (args.length < 5) { + System.out.println("Usage: osmtoshp neo4jHome database exportdir osmdataset layerspec <..layerspecs..>"); + System.out.println( + "\tNote: 'database' can only be something other than 'neo4j' in Neo4j Enterprise Edition."); + } else { + String homeDir = args[0]; + String database = args[1]; + String exportDir = args[2]; + String osmdataset = args[3]; + List layerspecs = new ArrayList<>(Arrays.asList(args).subList(4, args.length)); + DatabaseManagementService databases = new DatabaseManagementServiceBuilder(Path.of(homeDir)).build(); + GraphDatabaseService db = databases.database(database); + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); + OSMLayer layer; + try (Transaction tx = db.beginTx()) { + layer = (OSMLayer) spatial.getLayer(tx, osmdataset); + } + if (layer != null) { + ShapefileExporter exporter = new ShapefileExporter(db); + exporter.setExportDir(args[1] + File.separator + layer.getName()); + for (String layerspec : layerspecs) { + String[] fields = layerspec.split("[.\\-]"); + HashMap tags = new HashMap<>(); + String key = fields[0]; + String name = key; + if (fields.length > 1) { + String value = fields[1]; + name = key + "-" + value; + tags.put(key, value); + } + + try (Transaction tx = db.beginTx()) { + if (layer.getLayerNames(tx).contains(name)) { + System.out.println("Exporting previously existing layer: " + name); + } else { + System.out.println("Creating and exporting new layer: " + name); + layer.addDynamicLayerOnWayTags(tx, name, Constants.GTYPE_LINESTRING, tags); + } + exporter.exportLayer(name); + tx.commit(); + } catch (Exception e) { + System.err.println("Failed to export dynamic layer " + name + ": " + e); + e.printStackTrace(System.err); + } + } + } else { + System.err.println("No such layer: " + args[2]); + } + databases.shutdown(); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/osm/OSMRelation.java b/src/main/java/org/neo4j/gis/spatial/osm/OSMRelation.java index e12e79edf..310fb5d04 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/OSMRelation.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/OSMRelation.java @@ -35,5 +35,5 @@ import org.neo4j.graphdb.RelationshipType; public enum OSMRelation implements RelationshipType { - FIRST_NODE, LAST_NODE, OTHER, NEXT, OSM, WAYS, RELATIONS, MEMBERS, MEMBER, TAGS, GEOM, BBOX, NODE, CHANGESET, USER, USERS, OSM_USER; -} \ No newline at end of file + FIRST_NODE, LAST_NODE, OTHER, NEXT, OSM, WAYS, RELATIONS, MEMBERS, MEMBER, TAGS, GEOM, BBOX, NODE, CHANGESET, USER, USERS, OSM_USER; +} diff --git a/src/main/java/org/neo4j/gis/spatial/osm/RoadDirection.java b/src/main/java/org/neo4j/gis/spatial/osm/RoadDirection.java index 2e7759b41..e37ded9c7 100644 --- a/src/main/java/org/neo4j/gis/spatial/osm/RoadDirection.java +++ b/src/main/java/org/neo4j/gis/spatial/osm/RoadDirection.java @@ -33,5 +33,5 @@ package org.neo4j.gis.spatial.osm; public enum RoadDirection { - BOTH, FORWARD, BACKWARD; -} \ No newline at end of file + BOTH, FORWARD, BACKWARD; +} diff --git a/src/main/java/org/neo4j/gis/spatial/package-info.java b/src/main/java/org/neo4j/gis/spatial/package-info.java index 2e2c93ec4..b90f99e90 100644 --- a/src/main/java/org/neo4j/gis/spatial/package-info.java +++ b/src/main/java/org/neo4j/gis/spatial/package-info.java @@ -19,7 +19,7 @@ */ /** * Provides spatial indexing capabilities to the Neo4j graph. This component is a - * collection of various utilities for indexing parts of a Neo4j graph in + * collection of various utilities for indexing parts of a Neo4j graph in * different ways. Different APIs are supported, such as the GeoTools spatial API * and the Neo4j Index provider API. This component also makes it possible to * run Neo4j via GeoTools as a backend for GeoServer, Geomajas and uDig. diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractExtractGeoPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractExtractGeoPipe.java index 09d9555b2..8fc1b0994 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractExtractGeoPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractExtractGeoPipe.java @@ -28,32 +28,32 @@ */ public class AbstractExtractGeoPipe extends AbstractGeoPipe { - protected List extracts = new ArrayList(); - protected Iterator extractIterator = null; + protected List extracts = new ArrayList(); + protected Iterator extractIterator = null; - @Override - public GeoPipeFlow processNextStart() { - if (extractIterator != null) { - if (extractIterator.hasNext()) { - return extractIterator.next(); - } else { - extractIterator = null; - extracts.clear(); - } - } + @Override + public GeoPipeFlow processNextStart() { + if (extractIterator != null) { + if (extractIterator.hasNext()) { + return extractIterator.next(); + } else { + extractIterator = null; + extracts.clear(); + } + } - do { - extract(process(starts.next())); - } while (extracts.size() == 0); + do { + extract(process(starts.next())); + } while (extracts.size() == 0); - extractIterator = extracts.iterator(); - return extractIterator.next(); - } + extractIterator = extracts.iterator(); + return extractIterator.next(); + } - /** - * Subclasses should override this method - */ - protected void extract(GeoPipeFlow flow) { - extracts.add(flow); - } -} \ No newline at end of file + /** + * Subclasses should override this method + */ + protected void extract(GeoPipeFlow flow) { + extracts.add(flow); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractFilterGeoPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractFilterGeoPipe.java index 5be9cea2c..cae00a65b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractFilterGeoPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractFilterGeoPipe.java @@ -27,7 +27,7 @@ public abstract class AbstractFilterGeoPipe extends AbstractGeoPipe { protected AbstractFilterGeoPipe() { } - + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { if (validate(flow)) { @@ -43,4 +43,4 @@ protected GeoPipeFlow process(GeoPipeFlow flow) { protected boolean validate(GeoPipeFlow flow) { return true; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGeoPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGeoPipe.java index 8df56740e..fd54d3919 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGeoPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGeoPipe.java @@ -20,7 +20,6 @@ package org.neo4j.gis.spatial.pipes; import java.util.NoSuchElementException; - import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.impl.AbstractPipe; @@ -30,68 +29,68 @@ */ public abstract class AbstractGeoPipe extends AbstractPipe { - protected String resultPropertyName = null; + protected String resultPropertyName = null; - protected AbstractGeoPipe() { - } + protected AbstractGeoPipe() { + } - /** - * @param resultPropertyName name to use for the property containing Pipe output - */ - protected AbstractGeoPipe(String resultPropertyName) { - this(); - this.resultPropertyName = resultPropertyName; - } + /** + * @param resultPropertyName name to use for the property containing Pipe output + */ + protected AbstractGeoPipe(String resultPropertyName) { + this(); + this.resultPropertyName = resultPropertyName; + } - @Override - protected GeoPipeFlow processNextStart() throws NoSuchElementException { - GeoPipeFlow flow; - do { - flow = process(starts.next()); - } while (flow == null); + @Override + protected GeoPipeFlow processNextStart() throws NoSuchElementException { + GeoPipeFlow flow; + do { + flow = process(starts.next()); + } while (flow == null); - return flow; - } + return flow; + } - /** - * Subclasses should override this method. - */ - protected GeoPipeFlow process(GeoPipeFlow flow) { - return flow; - } + /** + * Subclasses should override this method. + */ + protected GeoPipeFlow process(GeoPipeFlow flow) { + return flow; + } - /** - * Puts pipe geometry output in the given GeoPipeFlow. - */ - protected void setGeometry(GeoPipeFlow flow, Geometry geometry) { - if (resultPropertyName != null) { - flow.getProperties().put(resultPropertyName, geometry); - } else { - flow.setGeometry(geometry); - } - } + /** + * Puts pipe geometry output in the given GeoPipeFlow. + */ + protected void setGeometry(GeoPipeFlow flow, Geometry geometry) { + if (resultPropertyName != null) { + flow.getProperties().put(resultPropertyName, geometry); + } else { + flow.setGeometry(geometry); + } + } - /** - * Puts pipe output in the given GeoPipeFlow. - */ - protected void setProperty(GeoPipeFlow flow, Object result) { - if (resultPropertyName != null) { - flow.getProperties().put(resultPropertyName, result); - } else { - flow.getProperties().put(generatePropertyName(), result); - } - } + /** + * Puts pipe output in the given GeoPipeFlow. + */ + protected void setProperty(GeoPipeFlow flow, Object result) { + if (resultPropertyName != null) { + flow.getProperties().put(resultPropertyName, result); + } else { + flow.getProperties().put(generatePropertyName(), result); + } + } - /** - * Creates a default property name, used if no name has been specified. - */ - protected String generatePropertyName() { - String className = getClass().getName(); - if (className.contains(".")) { - className = className.substring(className.lastIndexOf(".") + 1); - } + /** + * Creates a default property name, used if no name has been specified. + */ + protected String generatePropertyName() { + String className = getClass().getName(); + if (className.contains(".")) { + className = className.substring(className.lastIndexOf(".") + 1); + } - resultPropertyName = className; - return resultPropertyName; - } + resultPropertyName = className; + return resultPropertyName; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGroupGeoPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGroupGeoPipe.java index eff43c028..ebe829c83 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGroupGeoPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/AbstractGroupGeoPipe.java @@ -37,18 +37,18 @@ public GeoPipeFlow processNextStart() { group((GeoPipeFlow) starts.next()); } } catch (NoSuchElementException e) { - } - - groupIterator = groups.iterator(); - } - + } + + groupIterator = groups.iterator(); + } + return groupIterator.next(); } - + /** * Subclasses should override this method */ protected void group(GeoPipeFlow flow) { groups.add(flow); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeFlow.java b/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeFlow.java index ce70450ce..b15ee6ef7 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeFlow.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeFlow.java @@ -23,110 +23,108 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - -import org.neo4j.gis.spatial.SpatialDatabaseRecord; -import org.neo4j.gis.spatial.SpatialRecord; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.SpatialDatabaseRecord; +import org.neo4j.gis.spatial.SpatialRecord; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; public class GeoPipeFlow implements SpatialRecord { - private String id; - private List records = new ArrayList(); - private Geometry geometry; - private Envelope geometryEnvelope; - private Map properties = new HashMap<>(); - - private GeoPipeFlow(String id) { - this.id = id; - } - - public GeoPipeFlow(SpatialDatabaseRecord record) { - this.id = record.getNodeId(); - this.records.add(record); - this.geometry = record.getGeometry(); - } - - public SpatialDatabaseRecord getRecord() { - return records.get(0); - } - - @Override - public Node getGeomNode() { - return getRecord().getGeomNode(); - } - - public int countRecords() { - return records.size(); - } - - public List getRecords() { - return records; - } - - @Override - public String getId() { - return id; - } - - @Override - public Geometry getGeometry() { - return geometry; - } - - public Envelope getEnvelope() { - if (geometryEnvelope == null) { - geometryEnvelope = geometry.getEnvelopeInternal(); - } - - return geometryEnvelope; - } - - public void setGeometry(Geometry geometry) { - this.geometry = geometry; - this.geometryEnvelope = null; - } - - @Override - public Map getProperties(Transaction ignored) { - return properties; - } - - // Alternative method since GeoPipes never work within a transactional context - public Map getProperties() { - return properties; - } - - @Override - public boolean hasProperty(Transaction tx, String name) { - return properties.containsKey(name); - } - - @Override - public String[] getPropertyNames(Transaction tx) { - return properties.keySet().toArray(new String[]{}); - } - - @Override - public Object getProperty(Transaction ignored, String name) { - return properties.get(name); - } - - public void merge(GeoPipeFlow other) { - records.addAll(other.records); - // TODO id? - // TODO properties? - } - - public GeoPipeFlow makeClone(String idSuffix) { - // we don't need a deeper copy at the moment - GeoPipeFlow clone = new GeoPipeFlow(id + "-" + idSuffix); - clone.records.addAll(records); - clone.geometry = geometry; - clone.getProperties().putAll(getProperties()); - return clone; - } + private String id; + private List records = new ArrayList(); + private Geometry geometry; + private Envelope geometryEnvelope; + private Map properties = new HashMap<>(); + + private GeoPipeFlow(String id) { + this.id = id; + } + + public GeoPipeFlow(SpatialDatabaseRecord record) { + this.id = record.getNodeId(); + this.records.add(record); + this.geometry = record.getGeometry(); + } + + public SpatialDatabaseRecord getRecord() { + return records.get(0); + } + + @Override + public Node getGeomNode() { + return getRecord().getGeomNode(); + } + + public int countRecords() { + return records.size(); + } + + public List getRecords() { + return records; + } + + @Override + public String getId() { + return id; + } + + @Override + public Geometry getGeometry() { + return geometry; + } + + public Envelope getEnvelope() { + if (geometryEnvelope == null) { + geometryEnvelope = geometry.getEnvelopeInternal(); + } + + return geometryEnvelope; + } + + public void setGeometry(Geometry geometry) { + this.geometry = geometry; + this.geometryEnvelope = null; + } + + @Override + public Map getProperties(Transaction ignored) { + return properties; + } + + // Alternative method since GeoPipes never work within a transactional context + public Map getProperties() { + return properties; + } + + @Override + public boolean hasProperty(Transaction tx, String name) { + return properties.containsKey(name); + } + + @Override + public String[] getPropertyNames(Transaction tx) { + return properties.keySet().toArray(new String[]{}); + } + + @Override + public Object getProperty(Transaction ignored, String name) { + return properties.get(name); + } + + public void merge(GeoPipeFlow other) { + records.addAll(other.records); + // TODO id? + // TODO properties? + } + + public GeoPipeFlow makeClone(String idSuffix) { + // we don't need a deeper copy at the moment + GeoPipeFlow clone = new GeoPipeFlow(id + "-" + idSuffix); + clone.records.addAll(records); + clone.geometry = geometry; + clone.getProperties().putAll(getProperties()); + return clone; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeline.java b/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeline.java index 7ea8c9471..a7851b28c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeline.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/GeoPipeline.java @@ -19,876 +19,957 @@ */ package org.neo4j.gis.spatial.pipes; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.util.AffineTransformation; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.feature.FeatureCollection; import org.geotools.feature.collection.AbstractFeatureCollection; import org.geotools.filter.text.cql2.CQLException; import org.geotools.geometry.jts.ReferencedEnvelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.util.AffineTransformation; import org.neo4j.gis.spatial.Layer; import org.neo4j.gis.spatial.SpatialDatabaseRecord; import org.neo4j.gis.spatial.SpatialRecord; import org.neo4j.gis.spatial.SpatialTopologyUtils; import org.neo4j.gis.spatial.filter.SearchIntersectWindow; import org.neo4j.gis.spatial.filter.SearchRecords; -import org.neo4j.gis.spatial.pipes.filtering.*; -import org.neo4j.gis.spatial.pipes.impl.*; -import org.neo4j.gis.spatial.pipes.processing.*; +import org.neo4j.gis.spatial.pipes.filtering.FilterCQL; +import org.neo4j.gis.spatial.pipes.filtering.FilterContain; +import org.neo4j.gis.spatial.pipes.filtering.FilterCover; +import org.neo4j.gis.spatial.pipes.filtering.FilterCoveredBy; +import org.neo4j.gis.spatial.pipes.filtering.FilterCross; +import org.neo4j.gis.spatial.pipes.filtering.FilterDisjoint; +import org.neo4j.gis.spatial.pipes.filtering.FilterEmpty; +import org.neo4j.gis.spatial.pipes.filtering.FilterEqualExact; +import org.neo4j.gis.spatial.pipes.filtering.FilterEqualNorm; +import org.neo4j.gis.spatial.pipes.filtering.FilterEqualTopo; +import org.neo4j.gis.spatial.pipes.filtering.FilterInRelation; +import org.neo4j.gis.spatial.pipes.filtering.FilterIntersect; +import org.neo4j.gis.spatial.pipes.filtering.FilterIntersectWindow; +import org.neo4j.gis.spatial.pipes.filtering.FilterInvalid; +import org.neo4j.gis.spatial.pipes.filtering.FilterOverlap; +import org.neo4j.gis.spatial.pipes.filtering.FilterProperty; +import org.neo4j.gis.spatial.pipes.filtering.FilterPropertyNotNull; +import org.neo4j.gis.spatial.pipes.filtering.FilterPropertyNull; +import org.neo4j.gis.spatial.pipes.filtering.FilterTouch; +import org.neo4j.gis.spatial.pipes.filtering.FilterValid; +import org.neo4j.gis.spatial.pipes.filtering.FilterWithin; +import org.neo4j.gis.spatial.pipes.impl.FilterPipe; +import org.neo4j.gis.spatial.pipes.impl.IdentityPipe; +import org.neo4j.gis.spatial.pipes.impl.Pipe; +import org.neo4j.gis.spatial.pipes.impl.Pipeline; +import org.neo4j.gis.spatial.pipes.impl.RangeFilterPipe; +import org.neo4j.gis.spatial.pipes.processing.ApplyAffineTransformation; +import org.neo4j.gis.spatial.pipes.processing.Area; +import org.neo4j.gis.spatial.pipes.processing.Boundary; +import org.neo4j.gis.spatial.pipes.processing.Buffer; +import org.neo4j.gis.spatial.pipes.processing.Centroid; +import org.neo4j.gis.spatial.pipes.processing.ConvexHull; +import org.neo4j.gis.spatial.pipes.processing.CopyDatabaseRecordProperties; +import org.neo4j.gis.spatial.pipes.processing.Densify; +import org.neo4j.gis.spatial.pipes.processing.DensityIslands; +import org.neo4j.gis.spatial.pipes.processing.Difference; +import org.neo4j.gis.spatial.pipes.processing.Dimension; +import org.neo4j.gis.spatial.pipes.processing.Distance; +import org.neo4j.gis.spatial.pipes.processing.EndPoint; +import org.neo4j.gis.spatial.pipes.processing.ExtractGeometries; +import org.neo4j.gis.spatial.pipes.processing.ExtractPoints; +import org.neo4j.gis.spatial.pipes.processing.GML; +import org.neo4j.gis.spatial.pipes.processing.GeoJSON; +import org.neo4j.gis.spatial.pipes.processing.GeometryType; +import org.neo4j.gis.spatial.pipes.processing.InteriorPoint; +import org.neo4j.gis.spatial.pipes.processing.IntersectAll; +import org.neo4j.gis.spatial.pipes.processing.Intersection; +import org.neo4j.gis.spatial.pipes.processing.KeyholeMarkupLanguage; +import org.neo4j.gis.spatial.pipes.processing.Length; +import org.neo4j.gis.spatial.pipes.processing.Max; +import org.neo4j.gis.spatial.pipes.processing.Min; +import org.neo4j.gis.spatial.pipes.processing.NumGeometries; +import org.neo4j.gis.spatial.pipes.processing.NumPoints; +import org.neo4j.gis.spatial.pipes.processing.OrthodromicDistance; +import org.neo4j.gis.spatial.pipes.processing.OrthodromicLength; +import org.neo4j.gis.spatial.pipes.processing.SimplifyPreservingTopology; +import org.neo4j.gis.spatial.pipes.processing.SimplifyWithDouglasPeucker; +import org.neo4j.gis.spatial.pipes.processing.Sort; +import org.neo4j.gis.spatial.pipes.processing.StartPoint; +import org.neo4j.gis.spatial.pipes.processing.SymDifference; +import org.neo4j.gis.spatial.pipes.processing.Union; +import org.neo4j.gis.spatial.pipes.processing.UnionAll; +import org.neo4j.gis.spatial.pipes.processing.WellKnownText; import org.neo4j.gis.spatial.rtree.filter.SearchAll; import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.api.feature.simple.SimpleFeatureType; - -import java.io.IOException; -import java.util.*; public class GeoPipeline extends Pipeline { - protected Layer layer; - - protected GeoPipeline(Layer layer) { - this.layer = layer; - } - - protected static IdentityPipe createStartPipe(List records) { - return createStartPipe(records.iterator()); - } - - protected static IdentityPipe createStartPipe(final Iterator records) { - final Iterator start = new Iterator<>() { - @Override - public boolean hasNext() { - return records.hasNext(); - } - - @Override - public GeoPipeFlow next() { - return new GeoPipeFlow(records.next()); - } - - @Override - public void remove() { - records.remove(); - } - }; - return new IdentityPipe<>() { - { - super.setStarts(start); - } - }; - } - - /** - * Start a new pipeline with an iterator of SpatialDatabaseRecords - */ - public static GeoPipeline start(Layer layer, Iterator records) { - GeoPipeline pipeline = new GeoPipeline(layer); - return pipeline.add(createStartPipe(records)); - } - - /** - * Start a new pipeline with a list of SpatialDatabaseRecords - */ - public static GeoPipeline start(Layer layer, List records) { - GeoPipeline pipeline = new GeoPipeline(layer); - return pipeline.add(createStartPipe(records)); - } - - /** - * Start a new pipeline that will iterate through a SearchRecords - */ - public static GeoPipeline start(Layer layer, SearchRecords records) { - GeoPipeline pipeline = new GeoPipeline(layer); - return pipeline.add(createStartPipe(records)); - } - - /** - * Start a new pipeline that will iterate through a SearchFilter - */ - public static GeoPipeline start(final Transaction tx, Layer layer, SearchFilter searchFilter) { - return start(layer, layer.getIndex().search(tx, searchFilter)); - } - - /** - * Start a new pipeline that will iterate through all items contained in a Layer - */ - public static GeoPipeline start(final Transaction tx, Layer layer) { - return start(tx, layer, new SearchAll()); - } - - /** - * Extracts layer items that are found in the search window - */ - public static GeoPipeline startIntersectWindowSearch(final Transaction tx, Layer layer, Envelope searchWindow) { - return start(layer, layer.getIndex().search(tx, new SearchIntersectWindow(layer, searchWindow))); - } - - /** - * Extracts Layer items that contain the given geometry and start a pipeline. - */ - public static GeoPipeline startContainSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).containFilter(geometry); - } - - /** - * Extracts Layer items that cover the given geometry and start a pipeline. - */ - public static GeoPipeline startCoverSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).coverFilter(geometry); - } - - /** - * Extracts Layer items that are covered by the given geometry and start a pipeline. - */ - public static GeoPipeline startCoveredBySearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).coveredByFilter(geometry); - } - - /** - * Extracts Layer items that cross by the given geometry and start a pipeline. - */ - public static GeoPipeline startCrossSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).crossFilter(geometry); - } - - /** - * Extracts Layer items that are equal to the given geometry and start a pipeline. - */ - public static GeoPipeline startEqualExactSearch(final Transaction tx, Layer layer, Geometry geometry, double tolerance) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()) - .equalExactFilter(geometry, tolerance); - } - - /** - * Extracts Layer items that intersect the given geometry and start a pipeline. - */ - public static GeoPipeline startIntersectSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()) - .intersectionFilter(geometry); - } - - /** - * Extracts Layer items that overlap the given geometry and start a pipeline. - */ - public static GeoPipeline startOverlapSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).overlapFilter(geometry); - } - - /** - * Extracts Layer items that touch the given geometry and start a pipeline. - */ - public static GeoPipeline startTouchSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).touchFilter(geometry); - } - - /** - * Extracts Layer items that are within the given geometry and start a pipeline. - */ - public static GeoPipeline startWithinSearch(final Transaction tx, Layer layer, Geometry geometry) { - return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).withinFilter(geometry); - } - - /** - * Calculates the distance between Layer items nearest to the given point and the given point. - * The search window created is based on Layer items density and it could lead to no results. - * - * @param layer with latitude, longitude coordinates - * @param point - * @param numberOfItemsToFind tries to find this number of items for comparison - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, int numberOfItemsToFind) { - Envelope searchWindow = SpatialTopologyUtils.createEnvelopeForGeometryDensityEstimate(tx, layer, point, numberOfItemsToFind); - return startNearestNeighborLatLonSearch(tx, layer, point, searchWindow); - } - - /** - * Calculates the distance between Layer items inside the given search window and the given point. - * - * @param layer with latitude, longitude coordinates - * @param point - * @param searchWindow - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, Envelope searchWindow) { - return start(tx, layer, new SearchIntersectWindow(layer, searchWindow)).calculateOrthodromicDistance(point); - } - - /** - * Extracts Layer items with a distance from the given point that is less than or equal the given distance. - * - * @param layer with latitude, longitude coordinates - * @param point - * @param maxDistanceInKm - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, double maxDistanceInKm) { - Envelope searchWindow = OrthodromicDistance.suggestSearchWindow(point, maxDistanceInKm); - GeoPipeline pipeline = start(tx, layer, new SearchIntersectWindow(layer, searchWindow)).calculateOrthodromicDistance(point); - return pipeline.propertyFilter(OrthodromicDistance.DISTANCE, maxDistanceInKm, FilterPipe.Filter.LESS_THAN_EQUAL); - } - - /** - * Calculates the distance between Layer items nearest to the given point and the given point. - * The search window created is based on Layer items density and it could lead to no results. - * - * @param layer - * @param point - * @param numberOfItemsToFind tries to find this number of items for comparison - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, int numberOfItemsToFind) { - Envelope searchWindow = SpatialTopologyUtils.createEnvelopeForGeometryDensityEstimate(tx, layer, point, numberOfItemsToFind); - return startNearestNeighborSearch(tx, layer, point, searchWindow); - } - - /** - * Calculates the distance between Layer items inside the given search window and the given point. - * - * @param layer - * @param point - * @param searchWindow - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, Envelope searchWindow) { - return start(tx, layer, new SearchIntersectWindow(layer, searchWindow)).calculateDistance(layer.getGeometryFactory().createPoint(point)); - } - - /** - * Extracts Layer items with a distance from the given point that is less than or equal the given distance. - * - * @param layer - * @param point - * @param maxDistance - * @return geoPipeline - */ - public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, double maxDistance) { - Envelope extent = new Envelope(point.x - maxDistance, point.x + maxDistance, - point.y - maxDistance, point.y + maxDistance); - - return start(tx, layer, new SearchIntersectWindow(layer, extent)) - .calculateDistance(layer.getGeometryFactory().createPoint(point)) - .propertyFilter("Distance", maxDistance, FilterPipe.Filter.LESS_THAN_EQUAL); - } - - /** - * Adds a pipe at the end of this pipeline - * - * @param geoPipe - * @return geoPipeline - */ - public GeoPipeline addPipe(AbstractGeoPipe geoPipe) { - return add(geoPipe); - } - - /** - * @see CopyDatabaseRecordProperties - */ - public GeoPipeline copyDatabaseRecordProperties(Transaction tx) { - return addPipe(new CopyDatabaseRecordProperties(tx)); - } - - /** - * @see CopyDatabaseRecordProperties - */ - public GeoPipeline copyDatabaseRecordProperties(Transaction tx, String[] keys) { - return addPipe(new CopyDatabaseRecordProperties(tx, keys)); - } - - /** - * @see CopyDatabaseRecordProperties - */ - public GeoPipeline copyDatabaseRecordProperties(Transaction tx, String key) { - return addPipe(new CopyDatabaseRecordProperties(tx, key)); - } - - /** - * @see Min - */ - public GeoPipeline getMin(String property) { - return addPipe(new Min(property)); - } - - /** - * @see Max - */ - public GeoPipeline getMax(String property) { - return addPipe(new Max(property)); - } - - /** - * @see Sort - */ - public GeoPipeline sort(String property) { - return addPipe(new Sort(property, true)); - } - - /** - * @see Sort - */ - public GeoPipeline sort(String property, boolean asc) { - return addPipe(new Sort(property, asc)); - } - - /** - * @see Sort - */ - public GeoPipeline sort(String property, Comparator comparator) { - return addPipe(new Sort(property, comparator)); - } - - /** - * @see Boundary - */ - public GeoPipeline toBoundary() { - return addPipe(new Boundary()); - } - - /** - * @see Buffer - */ - public GeoPipeline toBuffer(double distance) { - return addPipe(new Buffer(distance)); - } - - /** - * @see Centroid - */ - public GeoPipeline toCentroid() { - return addPipe(new Centroid()); - } - - /** - * @see ConvexHull - */ - public GeoPipeline toConvexHull() { - return addPipe(new ConvexHull()); - } - - /** - * @see org.neo4j.gis.spatial.pipes.processing.Envelope - */ - public GeoPipeline toEnvelope() { - return addPipe(new org.neo4j.gis.spatial.pipes.processing.Envelope()); - } - - /** - * @see InteriorPoint - */ - public GeoPipeline toInteriorPoint() { - return addPipe(new InteriorPoint()); - } - - /** - * @see StartPoint - */ - public GeoPipeline toStartPoint() { - return addPipe(new StartPoint(layer.getGeometryFactory())); - } - - /** - * @see EndPoint - */ - public GeoPipeline toEndPoint() { - return addPipe(new EndPoint(layer.getGeometryFactory())); - } - - /** - * @see NumPoints - */ - public GeoPipeline countPoints() { - return addPipe(new NumPoints()); - } - - /** - * @see Union - */ - public GeoPipeline union() { - return addPipe(new Union()); - } - - /** - * @see Union - */ - public GeoPipeline union(Geometry geometry) { - return addPipe(new Union(geometry)); - } - - /** - * @see UnionAll - */ - public GeoPipeline unionAll() { - return addPipe(new UnionAll()); - } - - /** - * @see Intersection - */ - public GeoPipeline intersect(Geometry geometry) { - return addPipe(new Intersection(geometry)); - } - - /** - * @see IntersectAll - */ - public GeoPipeline intersectAll() { - return addPipe(new IntersectAll()); - } - - /** - * @see Difference - */ - public GeoPipeline difference(Geometry geometry) { - return addPipe(new Difference(geometry)); - } - - /** - * @see SymDifference - */ - public GeoPipeline symDifference(Geometry geometry) { - return addPipe(new SymDifference(geometry)); - } - - /** - * @see SimplifyWithDouglasPeucker - */ - public GeoPipeline simplifyWithDouglasPeucker(double distanceTolerance) { - return addPipe(new SimplifyWithDouglasPeucker(distanceTolerance)); - } - - /** - * @see SimplifyPreservingTopology - */ - public GeoPipeline simplifyPreservingTopology(double distanceTolerance) { - return addPipe(new SimplifyPreservingTopology(distanceTolerance)); - } - - /** - * @see ApplyAffineTransformation - */ - public GeoPipeline applyAffineTransform(AffineTransformation t) { - return addPipe(new ApplyAffineTransformation(t)); - } - - /** - * @see Densify - */ - public GeoPipeline densify(double distanceTolerance) { - return addPipe(new Densify(distanceTolerance)); - } - - /** - * @see Area - */ - public GeoPipeline calculateArea() { - return addPipe(new Area()); - } - - /** - * @see Length - */ - public GeoPipeline calculateLength() { - return addPipe(new Length()); - } - - /** - * @see OrthodromicLength - */ - public GeoPipeline calculateOrthodromicLength(Transaction tx) { - return addPipe(new OrthodromicLength(layer.getCoordinateReferenceSystem(tx))); - } - - /** - * @see Distance - */ - public GeoPipeline calculateDistance(Geometry reference) { - return addPipe(new Distance(reference)); - } - - /** - * @see OrthodromicDistance - */ - public GeoPipeline calculateOrthodromicDistance(Coordinate reference) { - return addPipe(new OrthodromicDistance(reference)); - } - - /** - * @see Dimension - */ - public GeoPipeline getDimension() { - return addPipe(new Dimension()); - } - - /** - * @see GeometryType - */ - public GeoPipeline getGeometryType() { - return addPipe(new GeometryType()); - } - - /** - * @see NumGeometries - */ - public GeoPipeline getNumGeometries() { - return addPipe(new NumGeometries()); - } - - /** - * @see GeoJSON - */ - public GeoPipeline createJson() { - return addPipe(new GeoJSON()); - } - - /** - * @see WellKnownText - */ - public GeoPipeline createWellKnownText() { - return addPipe(new WellKnownText()); - } - - /** - * @see KeyholeMarkupLanguage - */ - public GeoPipeline createKML() { - return addPipe(new KeyholeMarkupLanguage()); - } - - /** - * @see GML - */ - public GeoPipeline createGML() { - return addPipe(new GML()); - } - - /** - * @see FilterProperty - */ - public GeoPipeline propertyFilter(String key, Object value) { - return addPipe(new FilterProperty(key, value)); - } - - /** - * @see FilterProperty - */ - public GeoPipeline propertyFilter(String key, Object value, FilterPipe.Filter comparison) { - return addPipe(new FilterProperty(key, value, comparison)); - } - - /** - * @see FilterPropertyNotNull - */ - public GeoPipeline propertyNotNullFilter(String key) { - return addPipe(new FilterPropertyNotNull(key)); - } - - /** - * @see FilterPropertyNull - */ - public GeoPipeline propertyNullFilter(String key) { - return addPipe(new FilterPropertyNull(key)); - } - - /** - * @see FilterCQL - */ - public GeoPipeline cqlFilter(Transaction tx, String cql) throws CQLException { - return addPipe(new FilterCQL(tx, layer, cql)); - } - - /** - * @see FilterIntersect - */ - public GeoPipeline intersectionFilter(Geometry geometry) { - return addPipe(new FilterIntersect(geometry)); - } - - /** - * @see FilterIntersectWindow - */ - public GeoPipeline windowIntersectionFilter(double xmin, double ymin, double xmax, double ymax) { - return addPipe(new FilterIntersectWindow(layer.getGeometryFactory(), xmin, ymin, xmax, ymax)); - } - - /** - * @see FilterIntersectWindow - */ - public GeoPipeline windowIntersectionFilter(Envelope envelope) { - return addPipe(new FilterIntersectWindow(layer.getGeometryFactory(), envelope)); - } - - /** - * @see FilterContain - */ - public GeoPipeline containFilter(Geometry geometry) { - return addPipe(new FilterContain(geometry)); - } - - /** - * @see FilterCover - */ - public GeoPipeline coverFilter(Geometry geometry) { - return addPipe(new FilterCover(geometry)); - } - - /** - * @see FilterCoveredBy - */ - public GeoPipeline coveredByFilter(Geometry geometry) { - return addPipe(new FilterCoveredBy(geometry)); - } - - /** - * @see FilterCross - */ - public GeoPipeline crossFilter(Geometry geometry) { - return addPipe(new FilterCross(geometry)); - } - - /** - * @see FilterDisjoint - */ - public GeoPipeline disjointFilter(Geometry geometry) { - return addPipe(new FilterDisjoint(geometry)); - } - - /** - * @see FilterEmpty - */ - public GeoPipeline emptyFilter() { - return addPipe(new FilterEmpty()); - } - - /** - * @see FilterEqualExact - */ - public GeoPipeline equalExactFilter(Geometry geometry, double tolerance) { - return addPipe(new FilterEqualExact(geometry, tolerance)); - } - - /** - * @see FilterEqualNorm - */ - public GeoPipeline equalNormFilter(Geometry geometry, double tolerance) { - return addPipe(new FilterEqualNorm(geometry, tolerance)); - } - - /** - * @see FilterEqualTopo - */ - public GeoPipeline equalTopoFilter(Geometry geometry) { - return addPipe(new FilterEqualTopo(geometry)); - } - - /** - * @see FilterInRelation - */ - public GeoPipeline relationFilter(Geometry geometry, String intersectionPattern) { - return addPipe(new FilterInRelation(geometry, intersectionPattern)); - } - - /** - * @see FilterValid - */ - public GeoPipeline validFilter() { - return addPipe(new FilterValid()); - } - - /** - * @see FilterInvalid - */ - public GeoPipeline invalidFilter() { - return addPipe(new FilterInvalid()); - } - - /** - * @see FilterOverlap - */ - public GeoPipeline overlapFilter(Geometry geometry) { - return addPipe(new FilterOverlap(geometry)); - } - - /** - * @see FilterTouch - */ - public GeoPipeline touchFilter(Geometry geometry) { - return addPipe(new FilterTouch(geometry)); - } - - /** - * @see FilterWithin - */ - public GeoPipeline withinFilter(Geometry geometry) { - return addPipe(new FilterWithin(geometry)); - } - - /** - * @see DensityIslands - */ - public GeoPipeline groupByDensityIslands(double density) { - return addPipe(new DensityIslands(density)); - } - - /** - * @see ExtractPoints - */ - public GeoPipeline extractPoints() { - return addPipe(new ExtractPoints(layer.getGeometryFactory())); - } - - /** - * @see ExtractGeometries - */ - public GeoPipeline extractGeometries() { - return addPipe(new ExtractGeometries()); - } - - public FeatureCollection toStreamingFeatureCollection(final Transaction tx, final Envelope bounds) { - return toStreamingFeatureCollection(tx, Neo4jFeatureBuilder.getTypeFromLayer(tx, layer), bounds); - } - - public FeatureCollection toStreamingFeatureCollection(final Transaction tx, SimpleFeatureType featureType, final Envelope bounds) { - final Neo4jFeatureBuilder featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); - return new AbstractFeatureCollection(featureType) { - @Override - public int size() { - return Integer.MAX_VALUE; - } - - @Override - protected Iterator openIterator() { - return new Iterator() { - @Override - public boolean hasNext() { - return GeoPipeline.this.hasNext(); - } - - @Override - public SimpleFeature next() { - return featureBuilder.buildFeature(tx, GeoPipeline.this.next().getRecord()); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public ReferencedEnvelope getBounds() { - return new ReferencedEnvelope(bounds, layer.getCoordinateReferenceSystem(tx)); - } - }; - } - - public FeatureCollection toFeatureCollection(final Transaction tx) throws IOException { - return toFeatureCollection(tx, Neo4jFeatureBuilder.getTypeFromLayer(tx, layer)); - } - - public FeatureCollection toFeatureCollection(final Transaction tx, SimpleFeatureType featureType) { - final List records = toList(); - - Envelope bounds = null; - for (SpatialRecord record : records) { - if (bounds == null) { - bounds = record.getGeometry().getEnvelopeInternal(); - } else { - bounds.expandToInclude(record.getGeometry().getEnvelopeInternal()); - } - } - - final Iterator recordsIterator = records.iterator(); - final ReferencedEnvelope refBounds = new ReferencedEnvelope(bounds, layer.getCoordinateReferenceSystem(tx)); - - final Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, Arrays.asList(layer.getExtraPropertyNames(tx))); - return new AbstractFeatureCollection(featureType) { - @Override - public int size() { - return records.size(); - } - - @Override - protected Iterator openIterator() { - return new Iterator<>() { - - @Override - public boolean hasNext() { - return recordsIterator.hasNext(); - } - - @Override - public SimpleFeature next() { - return featureBuilder.buildFeature(tx, recordsIterator.next()); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public ReferencedEnvelope getBounds() { - return refBounds; - } - }; - } - - /** - * Iterates through the pipeline content and creates a list of all the SpatialDatabaseRecord found. - * This will empty the pipeline. - *

- * Warning: this method should not be used with pipes that extract many items from a single item - * or with pipes that group many items into fewer items. - *

- * Warning: GeoPipeline doesn't modify SpatialDatabaseRecords thus the geometries contained aren't those - * transformed by the pipeline but the original ones. - */ - public List toSpatialDatabaseRecordList() { - - List result = new ArrayList<>(); - - try { - while (true) { - result.add(next().getRecord()); - } - } catch (NoSuchElementException e) { - } - return result; - } - - /** - * Iterates through the pipeline content and creates a list of all the Nodes found. - * This will empty the pipeline. - *

- * Warning: this method should *not* be used with pipes that extract many items from a single item - * or with pipes that group many items into fewer items. - */ - public List toNodeList() { - List result = new ArrayList(); - try { - while (true) { - result.add(next().getRecord().getGeomNode()); - } - } catch (NoSuchElementException e) { - } - return result; - - } - - public GeoPipeline add(final Pipe pipe) { - this.addPipe(pipe); - return this; - } - - public GeoPipeline range(final int low, final int high) { - return this.add(new RangeFilterPipe(low, high)); - } + protected Layer layer; + + protected GeoPipeline(Layer layer) { + this.layer = layer; + } + + protected static IdentityPipe createStartPipe(List records) { + return createStartPipe(records.iterator()); + } + + protected static IdentityPipe createStartPipe(final Iterator records) { + final Iterator start = new Iterator<>() { + @Override + public boolean hasNext() { + return records.hasNext(); + } + + @Override + public GeoPipeFlow next() { + return new GeoPipeFlow(records.next()); + } + + @Override + public void remove() { + records.remove(); + } + }; + return new IdentityPipe<>() { + { + super.setStarts(start); + } + }; + } + + /** + * Start a new pipeline with an iterator of SpatialDatabaseRecords + */ + public static GeoPipeline start(Layer layer, Iterator records) { + GeoPipeline pipeline = new GeoPipeline(layer); + return pipeline.add(createStartPipe(records)); + } + + /** + * Start a new pipeline with a list of SpatialDatabaseRecords + */ + public static GeoPipeline start(Layer layer, List records) { + GeoPipeline pipeline = new GeoPipeline(layer); + return pipeline.add(createStartPipe(records)); + } + + /** + * Start a new pipeline that will iterate through a SearchRecords + */ + public static GeoPipeline start(Layer layer, SearchRecords records) { + GeoPipeline pipeline = new GeoPipeline(layer); + return pipeline.add(createStartPipe(records)); + } + + /** + * Start a new pipeline that will iterate through a SearchFilter + */ + public static GeoPipeline start(final Transaction tx, Layer layer, SearchFilter searchFilter) { + return start(layer, layer.getIndex().search(tx, searchFilter)); + } + + /** + * Start a new pipeline that will iterate through all items contained in a Layer + */ + public static GeoPipeline start(final Transaction tx, Layer layer) { + return start(tx, layer, new SearchAll()); + } + + /** + * Extracts layer items that are found in the search window + */ + public static GeoPipeline startIntersectWindowSearch(final Transaction tx, Layer layer, Envelope searchWindow) { + return start(layer, layer.getIndex().search(tx, new SearchIntersectWindow(layer, searchWindow))); + } + + /** + * Extracts Layer items that contain the given geometry and start a pipeline. + */ + public static GeoPipeline startContainSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).containFilter(geometry); + } + + /** + * Extracts Layer items that cover the given geometry and start a pipeline. + */ + public static GeoPipeline startCoverSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).coverFilter(geometry); + } + + /** + * Extracts Layer items that are covered by the given geometry and start a pipeline. + */ + public static GeoPipeline startCoveredBySearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).coveredByFilter(geometry); + } + + /** + * Extracts Layer items that cross by the given geometry and start a pipeline. + */ + public static GeoPipeline startCrossSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).crossFilter(geometry); + } + + /** + * Extracts Layer items that are equal to the given geometry and start a pipeline. + */ + public static GeoPipeline startEqualExactSearch(final Transaction tx, Layer layer, Geometry geometry, + double tolerance) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()) + .equalExactFilter(geometry, tolerance); + } + + /** + * Extracts Layer items that intersect the given geometry and start a pipeline. + */ + public static GeoPipeline startIntersectSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()) + .intersectionFilter(geometry); + } + + /** + * Extracts Layer items that overlap the given geometry and start a pipeline. + */ + public static GeoPipeline startOverlapSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).overlapFilter(geometry); + } + + /** + * Extracts Layer items that touch the given geometry and start a pipeline. + */ + public static GeoPipeline startTouchSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).touchFilter(geometry); + } + + /** + * Extracts Layer items that are within the given geometry and start a pipeline. + */ + public static GeoPipeline startWithinSearch(final Transaction tx, Layer layer, Geometry geometry) { + return startIntersectWindowSearch(tx, layer, geometry.getEnvelopeInternal()).withinFilter(geometry); + } + + /** + * Calculates the distance between Layer items nearest to the given point and the given point. + * The search window created is based on Layer items density and it could lead to no results. + * + * @param layer with latitude, longitude coordinates + * @param point + * @param numberOfItemsToFind tries to find this number of items for comparison + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, + int numberOfItemsToFind) { + Envelope searchWindow = SpatialTopologyUtils.createEnvelopeForGeometryDensityEstimate(tx, layer, point, + numberOfItemsToFind); + return startNearestNeighborLatLonSearch(tx, layer, point, searchWindow); + } + + /** + * Calculates the distance between Layer items inside the given search window and the given point. + * + * @param layer with latitude, longitude coordinates + * @param point + * @param searchWindow + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, + Envelope searchWindow) { + return start(tx, layer, new SearchIntersectWindow(layer, searchWindow)).calculateOrthodromicDistance(point); + } + + /** + * Extracts Layer items with a distance from the given point that is less than or equal the given distance. + * + * @param layer with latitude, longitude coordinates + * @param point + * @param maxDistanceInKm + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborLatLonSearch(final Transaction tx, Layer layer, Coordinate point, + double maxDistanceInKm) { + Envelope searchWindow = OrthodromicDistance.suggestSearchWindow(point, maxDistanceInKm); + GeoPipeline pipeline = start(tx, layer, + new SearchIntersectWindow(layer, searchWindow)).calculateOrthodromicDistance(point); + return pipeline.propertyFilter(OrthodromicDistance.DISTANCE, maxDistanceInKm, + FilterPipe.Filter.LESS_THAN_EQUAL); + } + + /** + * Calculates the distance between Layer items nearest to the given point and the given point. + * The search window created is based on Layer items density and it could lead to no results. + * + * @param layer + * @param point + * @param numberOfItemsToFind tries to find this number of items for comparison + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, + int numberOfItemsToFind) { + Envelope searchWindow = SpatialTopologyUtils.createEnvelopeForGeometryDensityEstimate(tx, layer, point, + numberOfItemsToFind); + return startNearestNeighborSearch(tx, layer, point, searchWindow); + } + + /** + * Calculates the distance between Layer items inside the given search window and the given point. + * + * @param layer + * @param point + * @param searchWindow + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, + Envelope searchWindow) { + return start(tx, layer, new SearchIntersectWindow(layer, searchWindow)).calculateDistance( + layer.getGeometryFactory().createPoint(point)); + } + + /** + * Extracts Layer items with a distance from the given point that is less than or equal the given distance. + * + * @param layer + * @param point + * @param maxDistance + * @return geoPipeline + */ + public static GeoPipeline startNearestNeighborSearch(final Transaction tx, Layer layer, Coordinate point, + double maxDistance) { + Envelope extent = new Envelope(point.x - maxDistance, point.x + maxDistance, + point.y - maxDistance, point.y + maxDistance); + + return start(tx, layer, new SearchIntersectWindow(layer, extent)) + .calculateDistance(layer.getGeometryFactory().createPoint(point)) + .propertyFilter("Distance", maxDistance, FilterPipe.Filter.LESS_THAN_EQUAL); + } + + /** + * Adds a pipe at the end of this pipeline + * + * @param geoPipe + * @return geoPipeline + */ + public GeoPipeline addPipe(AbstractGeoPipe geoPipe) { + return add(geoPipe); + } + + /** + * @see CopyDatabaseRecordProperties + */ + public GeoPipeline copyDatabaseRecordProperties(Transaction tx) { + return addPipe(new CopyDatabaseRecordProperties(tx)); + } + + /** + * @see CopyDatabaseRecordProperties + */ + public GeoPipeline copyDatabaseRecordProperties(Transaction tx, String[] keys) { + return addPipe(new CopyDatabaseRecordProperties(tx, keys)); + } + + /** + * @see CopyDatabaseRecordProperties + */ + public GeoPipeline copyDatabaseRecordProperties(Transaction tx, String key) { + return addPipe(new CopyDatabaseRecordProperties(tx, key)); + } + + /** + * @see Min + */ + public GeoPipeline getMin(String property) { + return addPipe(new Min(property)); + } + + /** + * @see Max + */ + public GeoPipeline getMax(String property) { + return addPipe(new Max(property)); + } + + /** + * @see Sort + */ + public GeoPipeline sort(String property) { + return addPipe(new Sort(property, true)); + } + + /** + * @see Sort + */ + public GeoPipeline sort(String property, boolean asc) { + return addPipe(new Sort(property, asc)); + } + + /** + * @see Sort + */ + public GeoPipeline sort(String property, Comparator comparator) { + return addPipe(new Sort(property, comparator)); + } + + /** + * @see Boundary + */ + public GeoPipeline toBoundary() { + return addPipe(new Boundary()); + } + + /** + * @see Buffer + */ + public GeoPipeline toBuffer(double distance) { + return addPipe(new Buffer(distance)); + } + + /** + * @see Centroid + */ + public GeoPipeline toCentroid() { + return addPipe(new Centroid()); + } + + /** + * @see ConvexHull + */ + public GeoPipeline toConvexHull() { + return addPipe(new ConvexHull()); + } + + /** + * @see org.neo4j.gis.spatial.pipes.processing.Envelope + */ + public GeoPipeline toEnvelope() { + return addPipe(new org.neo4j.gis.spatial.pipes.processing.Envelope()); + } + + /** + * @see InteriorPoint + */ + public GeoPipeline toInteriorPoint() { + return addPipe(new InteriorPoint()); + } + + /** + * @see StartPoint + */ + public GeoPipeline toStartPoint() { + return addPipe(new StartPoint(layer.getGeometryFactory())); + } + + /** + * @see EndPoint + */ + public GeoPipeline toEndPoint() { + return addPipe(new EndPoint(layer.getGeometryFactory())); + } + + /** + * @see NumPoints + */ + public GeoPipeline countPoints() { + return addPipe(new NumPoints()); + } + + /** + * @see Union + */ + public GeoPipeline union() { + return addPipe(new Union()); + } + + /** + * @see Union + */ + public GeoPipeline union(Geometry geometry) { + return addPipe(new Union(geometry)); + } + + /** + * @see UnionAll + */ + public GeoPipeline unionAll() { + return addPipe(new UnionAll()); + } + + /** + * @see Intersection + */ + public GeoPipeline intersect(Geometry geometry) { + return addPipe(new Intersection(geometry)); + } + + /** + * @see IntersectAll + */ + public GeoPipeline intersectAll() { + return addPipe(new IntersectAll()); + } + + /** + * @see Difference + */ + public GeoPipeline difference(Geometry geometry) { + return addPipe(new Difference(geometry)); + } + + /** + * @see SymDifference + */ + public GeoPipeline symDifference(Geometry geometry) { + return addPipe(new SymDifference(geometry)); + } + + /** + * @see SimplifyWithDouglasPeucker + */ + public GeoPipeline simplifyWithDouglasPeucker(double distanceTolerance) { + return addPipe(new SimplifyWithDouglasPeucker(distanceTolerance)); + } + + /** + * @see SimplifyPreservingTopology + */ + public GeoPipeline simplifyPreservingTopology(double distanceTolerance) { + return addPipe(new SimplifyPreservingTopology(distanceTolerance)); + } + + /** + * @see ApplyAffineTransformation + */ + public GeoPipeline applyAffineTransform(AffineTransformation t) { + return addPipe(new ApplyAffineTransformation(t)); + } + + /** + * @see Densify + */ + public GeoPipeline densify(double distanceTolerance) { + return addPipe(new Densify(distanceTolerance)); + } + + /** + * @see Area + */ + public GeoPipeline calculateArea() { + return addPipe(new Area()); + } + + /** + * @see Length + */ + public GeoPipeline calculateLength() { + return addPipe(new Length()); + } + + /** + * @see OrthodromicLength + */ + public GeoPipeline calculateOrthodromicLength(Transaction tx) { + return addPipe(new OrthodromicLength(layer.getCoordinateReferenceSystem(tx))); + } + + /** + * @see Distance + */ + public GeoPipeline calculateDistance(Geometry reference) { + return addPipe(new Distance(reference)); + } + + /** + * @see OrthodromicDistance + */ + public GeoPipeline calculateOrthodromicDistance(Coordinate reference) { + return addPipe(new OrthodromicDistance(reference)); + } + + /** + * @see Dimension + */ + public GeoPipeline getDimension() { + return addPipe(new Dimension()); + } + + /** + * @see GeometryType + */ + public GeoPipeline getGeometryType() { + return addPipe(new GeometryType()); + } + + /** + * @see NumGeometries + */ + public GeoPipeline getNumGeometries() { + return addPipe(new NumGeometries()); + } + + /** + * @see GeoJSON + */ + public GeoPipeline createJson() { + return addPipe(new GeoJSON()); + } + + /** + * @see WellKnownText + */ + public GeoPipeline createWellKnownText() { + return addPipe(new WellKnownText()); + } + + /** + * @see KeyholeMarkupLanguage + */ + public GeoPipeline createKML() { + return addPipe(new KeyholeMarkupLanguage()); + } + + /** + * @see GML + */ + public GeoPipeline createGML() { + return addPipe(new GML()); + } + + /** + * @see FilterProperty + */ + public GeoPipeline propertyFilter(String key, Object value) { + return addPipe(new FilterProperty(key, value)); + } + + /** + * @see FilterProperty + */ + public GeoPipeline propertyFilter(String key, Object value, FilterPipe.Filter comparison) { + return addPipe(new FilterProperty(key, value, comparison)); + } + + /** + * @see FilterPropertyNotNull + */ + public GeoPipeline propertyNotNullFilter(String key) { + return addPipe(new FilterPropertyNotNull(key)); + } + + /** + * @see FilterPropertyNull + */ + public GeoPipeline propertyNullFilter(String key) { + return addPipe(new FilterPropertyNull(key)); + } + + /** + * @see FilterCQL + */ + public GeoPipeline cqlFilter(Transaction tx, String cql) throws CQLException { + return addPipe(new FilterCQL(tx, layer, cql)); + } + + /** + * @see FilterIntersect + */ + public GeoPipeline intersectionFilter(Geometry geometry) { + return addPipe(new FilterIntersect(geometry)); + } + + /** + * @see FilterIntersectWindow + */ + public GeoPipeline windowIntersectionFilter(double xmin, double ymin, double xmax, double ymax) { + return addPipe(new FilterIntersectWindow(layer.getGeometryFactory(), xmin, ymin, xmax, ymax)); + } + + /** + * @see FilterIntersectWindow + */ + public GeoPipeline windowIntersectionFilter(Envelope envelope) { + return addPipe(new FilterIntersectWindow(layer.getGeometryFactory(), envelope)); + } + + /** + * @see FilterContain + */ + public GeoPipeline containFilter(Geometry geometry) { + return addPipe(new FilterContain(geometry)); + } + + /** + * @see FilterCover + */ + public GeoPipeline coverFilter(Geometry geometry) { + return addPipe(new FilterCover(geometry)); + } + + /** + * @see FilterCoveredBy + */ + public GeoPipeline coveredByFilter(Geometry geometry) { + return addPipe(new FilterCoveredBy(geometry)); + } + + /** + * @see FilterCross + */ + public GeoPipeline crossFilter(Geometry geometry) { + return addPipe(new FilterCross(geometry)); + } + + /** + * @see FilterDisjoint + */ + public GeoPipeline disjointFilter(Geometry geometry) { + return addPipe(new FilterDisjoint(geometry)); + } + + /** + * @see FilterEmpty + */ + public GeoPipeline emptyFilter() { + return addPipe(new FilterEmpty()); + } + + /** + * @see FilterEqualExact + */ + public GeoPipeline equalExactFilter(Geometry geometry, double tolerance) { + return addPipe(new FilterEqualExact(geometry, tolerance)); + } + + /** + * @see FilterEqualNorm + */ + public GeoPipeline equalNormFilter(Geometry geometry, double tolerance) { + return addPipe(new FilterEqualNorm(geometry, tolerance)); + } + + /** + * @see FilterEqualTopo + */ + public GeoPipeline equalTopoFilter(Geometry geometry) { + return addPipe(new FilterEqualTopo(geometry)); + } + + /** + * @see FilterInRelation + */ + public GeoPipeline relationFilter(Geometry geometry, String intersectionPattern) { + return addPipe(new FilterInRelation(geometry, intersectionPattern)); + } + + /** + * @see FilterValid + */ + public GeoPipeline validFilter() { + return addPipe(new FilterValid()); + } + + /** + * @see FilterInvalid + */ + public GeoPipeline invalidFilter() { + return addPipe(new FilterInvalid()); + } + + /** + * @see FilterOverlap + */ + public GeoPipeline overlapFilter(Geometry geometry) { + return addPipe(new FilterOverlap(geometry)); + } + + /** + * @see FilterTouch + */ + public GeoPipeline touchFilter(Geometry geometry) { + return addPipe(new FilterTouch(geometry)); + } + + /** + * @see FilterWithin + */ + public GeoPipeline withinFilter(Geometry geometry) { + return addPipe(new FilterWithin(geometry)); + } + + /** + * @see DensityIslands + */ + public GeoPipeline groupByDensityIslands(double density) { + return addPipe(new DensityIslands(density)); + } + + /** + * @see ExtractPoints + */ + public GeoPipeline extractPoints() { + return addPipe(new ExtractPoints(layer.getGeometryFactory())); + } + + /** + * @see ExtractGeometries + */ + public GeoPipeline extractGeometries() { + return addPipe(new ExtractGeometries()); + } + + public FeatureCollection toStreamingFeatureCollection(final Transaction tx, + final Envelope bounds) { + return toStreamingFeatureCollection(tx, Neo4jFeatureBuilder.getTypeFromLayer(tx, layer), bounds); + } + + public FeatureCollection toStreamingFeatureCollection(final Transaction tx, + SimpleFeatureType featureType, final Envelope bounds) { + final Neo4jFeatureBuilder featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); + return new AbstractFeatureCollection(featureType) { + @Override + public int size() { + return Integer.MAX_VALUE; + } + + @Override + protected Iterator openIterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return GeoPipeline.this.hasNext(); + } + + @Override + public SimpleFeature next() { + return featureBuilder.buildFeature(tx, GeoPipeline.this.next().getRecord()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public ReferencedEnvelope getBounds() { + return new ReferencedEnvelope(bounds, layer.getCoordinateReferenceSystem(tx)); + } + }; + } + + public FeatureCollection toFeatureCollection(final Transaction tx) + throws IOException { + return toFeatureCollection(tx, Neo4jFeatureBuilder.getTypeFromLayer(tx, layer)); + } + + public FeatureCollection toFeatureCollection(final Transaction tx, + SimpleFeatureType featureType) { + final List records = toList(); + + Envelope bounds = null; + for (SpatialRecord record : records) { + if (bounds == null) { + bounds = record.getGeometry().getEnvelopeInternal(); + } else { + bounds.expandToInclude(record.getGeometry().getEnvelopeInternal()); + } + } + + final Iterator recordsIterator = records.iterator(); + final ReferencedEnvelope refBounds = new ReferencedEnvelope(bounds, layer.getCoordinateReferenceSystem(tx)); + + final Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, + Arrays.asList(layer.getExtraPropertyNames(tx))); + return new AbstractFeatureCollection(featureType) { + @Override + public int size() { + return records.size(); + } + + @Override + protected Iterator openIterator() { + return new Iterator<>() { + + @Override + public boolean hasNext() { + return recordsIterator.hasNext(); + } + + @Override + public SimpleFeature next() { + return featureBuilder.buildFeature(tx, recordsIterator.next()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public ReferencedEnvelope getBounds() { + return refBounds; + } + }; + } + + /** + * Iterates through the pipeline content and creates a list of all the SpatialDatabaseRecord found. + * This will empty the pipeline. + *

+ * Warning: this method should not be used with pipes that extract many items from a single item + * or with pipes that group many items into fewer items. + *

+ * Warning: GeoPipeline doesn't modify SpatialDatabaseRecords thus the geometries contained aren't those + * transformed by the pipeline but the original ones. + */ + public List toSpatialDatabaseRecordList() { + + List result = new ArrayList<>(); + + try { + while (true) { + result.add(next().getRecord()); + } + } catch (NoSuchElementException e) { + } + return result; + } + + /** + * Iterates through the pipeline content and creates a list of all the Nodes found. + * This will empty the pipeline. + *

+ * Warning: this method should *not* be used with pipes that extract many items from a single item + * or with pipes that group many items into fewer items. + */ + public List toNodeList() { + List result = new ArrayList(); + try { + while (true) { + result.add(next().getRecord().getGeomNode()); + } + } catch (NoSuchElementException e) { + } + return result; + + } + + public GeoPipeline add(final Pipe pipe) { + this.addPipe(pipe); + return this; + } + + public GeoPipeline range(final int low, final int high) { + return this.add(new RangeFilterPipe(low, high)); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCQL.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCQL.java index 1cb0af403..83f1a4d0c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCQL.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCQL.java @@ -19,6 +19,7 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.geotools.api.feature.simple.SimpleFeature; import org.geotools.api.filter.Filter; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.filter.text.cql2.CQLException; @@ -27,26 +28,25 @@ import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeature; /** * Filter geometries using a CQL query. */ public class FilterCQL extends AbstractFilterGeoPipe { - private final Neo4jFeatureBuilder featureBuilder; - private final Filter filter; - private final Transaction tx; + private final Neo4jFeatureBuilder featureBuilder; + private final Filter filter; + private final Transaction tx; - public FilterCQL(Transaction tx, Layer layer, String cqlPredicate) throws CQLException { - this.tx = tx; - this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); - this.filter = ECQL.toFilter(cqlPredicate); - } + public FilterCQL(Transaction tx, Layer layer, String cqlPredicate) throws CQLException { + this.tx = tx; + this.featureBuilder = Neo4jFeatureBuilder.fromLayer(tx, layer); + this.filter = ECQL.toFilter(cqlPredicate); + } - @Override - protected boolean validate(GeoPipeFlow flow) { - SimpleFeature feature = featureBuilder.buildFeature(tx, flow.getRecord()); - return filter.evaluate(feature); - } + @Override + protected boolean validate(GeoPipeFlow flow) { + SimpleFeature feature = featureBuilder.buildFeature(tx, flow.getRecord()); + return filter.evaluate(feature); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterContain.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterContain.java index bfcdf5030..4733b44cf 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterContain.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterContain.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -33,7 +32,7 @@ public class FilterContain extends AbstractFilterGeoPipe { private Geometry other; private Envelope otherEnvelope; - + public FilterContain(Geometry other) { this.other = other; this.otherEnvelope = other.getEnvelopeInternal(); @@ -42,9 +41,9 @@ public FilterContain(Geometry other) { @Override protected boolean validate(GeoPipeFlow flow) { // check if every point of the other geometry is a point of this geometry, - // and the interiors of the two geometries have at least one point in common - return flow.getEnvelope().contains(otherEnvelope) - && flow.getGeometry().contains(other); + // and the interiors of the two geometries have at least one point in common + return flow.getEnvelope().contains(otherEnvelope) + && flow.getGeometry().contains(other); } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCover.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCover.java index 7e3fef945..936913ad1 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCover.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCover.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -33,7 +32,7 @@ public class FilterCover extends AbstractFilterGeoPipe { private Geometry other; private Envelope otherEnvelope; - + public FilterCover(Geometry other) { this.other = other; this.otherEnvelope = other.getEnvelopeInternal(); diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCoveredBy.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCoveredBy.java index 7e5183ab3..e55ae6814 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCoveredBy.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCoveredBy.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -33,7 +32,7 @@ public class FilterCoveredBy extends AbstractFilterGeoPipe { private Geometry other; private Envelope otherEnvelope; - + public FilterCoveredBy(Geometry other) { this.other = other; this.otherEnvelope = other.getEnvelopeInternal(); @@ -42,8 +41,8 @@ public FilterCoveredBy(Geometry other) { @Override protected boolean validate(GeoPipeFlow flow) { // check if every point of this geometry is a point of the other geometry - return otherEnvelope.covers(flow.getEnvelope()) - && flow.getGeometry().coveredBy(other); + return otherEnvelope.covers(flow.getEnvelope()) + && flow.getGeometry().coveredBy(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCross.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCross.java index 1e71381d7..accc62efb 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCross.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterCross.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that have some but not all interior points in common with the given geometry @@ -31,7 +30,7 @@ public class FilterCross extends AbstractFilterGeoPipe { private Geometry other; - + public FilterCross(Geometry other) { this.other = other; } @@ -40,4 +39,4 @@ public FilterCross(Geometry other) { protected boolean validate(GeoPipeFlow flow) { return flow.getGeometry().crosses(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterDisjoint.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterDisjoint.java index a1b528ee7..b89e25ee8 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterDisjoint.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterDisjoint.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -33,15 +32,15 @@ public class FilterDisjoint extends AbstractFilterGeoPipe { private Geometry other; private Envelope otherEnvelope; - + public FilterDisjoint(Geometry other) { this.other = other; this.otherEnvelope = other.getEnvelopeInternal(); } - + @Override protected boolean validate(GeoPipeFlow flow) { return !flow.getEnvelope().intersects(otherEnvelope) || flow.getGeometry().disjoint(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEmpty.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEmpty.java index fd3e9ff17..b0b071222 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEmpty.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEmpty.java @@ -27,10 +27,10 @@ * Find empty geometries. */ public class FilterEmpty extends AbstractFilterGeoPipe { - + @Override protected boolean validate(GeoPipeFlow flow) { return flow.getGeometry().isEmpty(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualExact.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualExact.java index e87e2a34f..08e9c431c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualExact.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualExact.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries equal to the given geometry.
@@ -37,7 +36,7 @@ public class FilterEqualExact extends AbstractFilterGeoPipe { private Geometry other; private double tolerance; - + public FilterEqualExact(Geometry other) { this(other, 0); } @@ -45,8 +44,8 @@ public FilterEqualExact(Geometry other) { public FilterEqualExact(Geometry other, double tolerance) { this.other = other; this.tolerance = tolerance; - } - + } + @Override protected boolean validate(GeoPipeFlow flow) { return other.equalsExact(flow.getGeometry(), tolerance); diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualNorm.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualNorm.java index ac8d6da0e..03e587726 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualNorm.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualNorm.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries equal to the given geometry (with the same number of vertices, in the same locations).
@@ -34,7 +33,7 @@ public class FilterEqualNorm extends AbstractFilterGeoPipe { private Geometry other; private double tolerance; - + public FilterEqualNorm(Geometry other) { this(other, 0); } @@ -42,8 +41,8 @@ public FilterEqualNorm(Geometry other) { public FilterEqualNorm(Geometry other, double tolerance) { this.other = other.norm(); this.tolerance = tolerance; - } - + } + @Override protected boolean validate(GeoPipeFlow flow) { Geometry current = flow.getGeometry().norm(); diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualTopo.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualTopo.java index 9ca063886..bc20be292 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualTopo.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterEqualTopo.java @@ -19,17 +19,16 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries equal to the given geometry.
*
- * This filter tests for topological equality which is equivalent to drawing the two Geometry objects - * and seeing if all of their component edges overlap. It is the most robust kind of comparison but also + * This filter tests for topological equality which is equivalent to drawing the two Geometry objects + * and seeing if all of their component edges overlap. It is the most robust kind of comparison but also * the most computationally expensive.
*
* See GeoTools documentation. @@ -37,11 +36,11 @@ public class FilterEqualTopo extends AbstractFilterGeoPipe { private Geometry other; - + public FilterEqualTopo(Geometry other) { this.other = other; } - + @Override protected boolean validate(GeoPipeFlow flow) { return other.equalsTopo(flow.getGeometry()); diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInRelation.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInRelation.java index e6871fd67..693ea4660 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInRelation.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInRelation.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Returned geometries have the specified relation with the given geometry @@ -31,11 +30,12 @@ public class FilterInRelation extends AbstractFilterGeoPipe { private Geometry other; - private String intersectionPattern; - + private String intersectionPattern; + /** - * @param other geometry - * @param intersectionPattern a 9-character string (for more information on the DE-9IM, see the OpenGIS Simple Features Specification) + * @param other geometry + * @param intersectionPattern a 9-character string (for more information on the DE-9IM, see the OpenGIS Simple + * Features Specification) */ public FilterInRelation(Geometry other, String intersectionPattern) { this.other = other; diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersect.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersect.java index c91747879..2e6b72dc0 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersect.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersect.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that intersects the given geometry. @@ -31,13 +30,13 @@ public class FilterIntersect extends AbstractFilterGeoPipe { private Geometry geometry; - + public FilterIntersect(Geometry geometry) { this.geometry = geometry; - } - + } + @Override protected boolean validate(GeoPipeFlow flow) { return geometry.intersects(flow.getGeometry()); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersectWindow.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersectWindow.java index 9deea6696..dc48bf4d6 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersectWindow.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterIntersectWindow.java @@ -19,12 +19,11 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -34,19 +33,19 @@ public class FilterIntersectWindow extends AbstractFilterGeoPipe { private Envelope envelope; private Geometry envelopeGeom; - + public FilterIntersectWindow(GeometryFactory geomFactory, double xmin, double ymin, double xmax, double ymax) { this(geomFactory, new Envelope(xmin, xmax, ymin, ymax)); } - + public FilterIntersectWindow(GeometryFactory geomFactory, Envelope envelope) { this.envelope = envelope; this.envelopeGeom = geomFactory.toGeometry(envelope); - } - + } + @Override protected boolean validate(GeoPipeFlow flow) { - return envelope.intersects(flow.getEnvelope()) + return envelope.intersects(flow.getEnvelope()) && envelopeGeom.intersects(flow.getGeometry()); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInvalid.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInvalid.java index b432376ef..2dc36fca9 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInvalid.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterInvalid.java @@ -27,10 +27,10 @@ * Find invalid geometries. */ public class FilterInvalid extends AbstractFilterGeoPipe { - + @Override protected boolean validate(GeoPipeFlow flow) { return !flow.getGeometry().isValid(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterOverlap.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterOverlap.java index 9d323290c..36a8a7262 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterOverlap.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterOverlap.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that overlap the given geometry @@ -31,7 +30,7 @@ public class FilterOverlap extends AbstractFilterGeoPipe { private Geometry other; - + public FilterOverlap(Geometry other) { this.other = other; } @@ -44,4 +43,4 @@ protected boolean validate(GeoPipeFlow flow) { // the same dimension as the geometries themselves return flow.getGeometry().overlaps(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterProperty.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterProperty.java index 032fee352..bc7d8443a 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterProperty.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterProperty.java @@ -31,11 +31,11 @@ public class FilterProperty extends AbstractFilterGeoPipe { private String key; private Object value; private FilterPipe.Filter comparison; - + public FilterProperty(String key, Object value) { this(key, value, FilterPipe.Filter.EQUAL); - } - + } + public FilterProperty(String key, Object value, FilterPipe.Filter comparison) { this.key = key; this.value = value; @@ -44,34 +44,40 @@ public FilterProperty(String key, Object value, FilterPipe.Filter comparison) { @Override protected boolean validate(GeoPipeFlow flow) { - final Object leftObject = flow.getProperties().get(key); - switch (comparison) { - case EQUAL: - if (null == leftObject) - return value == null; - return leftObject.equals(value); - case NOT_EQUAL: - if (null == leftObject) - return value != null; - return !leftObject.equals(value); - case GREATER_THAN: - if (null == leftObject || value == null) - return false; - return ((Comparable) leftObject).compareTo(value) == 1; - case LESS_THAN: - if (null == leftObject || value == null) - return false; - return ((Comparable) leftObject).compareTo(value) == -1; - case GREATER_THAN_EQUAL: - if (null == leftObject || value == null) - return false; - return ((Comparable) leftObject).compareTo(value) >= 0; - case LESS_THAN_EQUAL: - if (null == leftObject || value == null) - return false; - return ((Comparable) leftObject).compareTo(value) <= 0; - default: - throw new IllegalArgumentException("Invalid state as no valid filter was provided"); - } - } -} \ No newline at end of file + final Object leftObject = flow.getProperties().get(key); + switch (comparison) { + case EQUAL: + if (null == leftObject) { + return value == null; + } + return leftObject.equals(value); + case NOT_EQUAL: + if (null == leftObject) { + return value != null; + } + return !leftObject.equals(value); + case GREATER_THAN: + if (null == leftObject || value == null) { + return false; + } + return ((Comparable) leftObject).compareTo(value) == 1; + case LESS_THAN: + if (null == leftObject || value == null) { + return false; + } + return ((Comparable) leftObject).compareTo(value) == -1; + case GREATER_THAN_EQUAL: + if (null == leftObject || value == null) { + return false; + } + return ((Comparable) leftObject).compareTo(value) >= 0; + case LESS_THAN_EQUAL: + if (null == leftObject || value == null) { + return false; + } + return ((Comparable) leftObject).compareTo(value) <= 0; + default: + throw new IllegalArgumentException("Invalid state as no valid filter was provided"); + } + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNotNull.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNotNull.java index 0ed349d45..9e3336949 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNotNull.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNotNull.java @@ -29,7 +29,7 @@ public class FilterPropertyNotNull extends AbstractFilterGeoPipe { private String property; - + public FilterPropertyNotNull(String property) { this.property = property; } @@ -38,4 +38,4 @@ public FilterPropertyNotNull(String property) { protected boolean validate(GeoPipeFlow flow) { return flow.getProperties().get(property) != null; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNull.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNull.java index 9bd171167..209744430 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNull.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterPropertyNull.java @@ -28,7 +28,7 @@ public class FilterPropertyNull extends AbstractFilterGeoPipe { private String property; - + public FilterPropertyNull(String property) { this.property = property; } @@ -37,4 +37,4 @@ public FilterPropertyNull(String property) { protected boolean validate(GeoPipeFlow flow) { return flow.getProperties().get(property) == null; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterTouch.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterTouch.java index 0d327f084..d9bc33fe2 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterTouch.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterTouch.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Find geometries that touch the given geometry. @@ -31,7 +30,7 @@ public class FilterTouch extends AbstractFilterGeoPipe { private Geometry other; - + public FilterTouch(Geometry other) { this.other = other; } @@ -41,4 +40,4 @@ protected boolean validate(GeoPipeFlow flow) { // if the geometries have at least one point in common, but their interiors do not intersect return flow.getGeometry().touches(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterValid.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterValid.java index fc87be01b..014cc3277 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterValid.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterValid.java @@ -28,10 +28,10 @@ * Find valid geometries. */ public class FilterValid extends AbstractFilterGeoPipe { - + @Override protected boolean validate(GeoPipeFlow flow) { return flow.getGeometry().isValid(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterWithin.java b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterWithin.java index eee8e4d87..d3e3ae934 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterWithin.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/filtering/FilterWithin.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.filtering; -import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractFilterGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** @@ -33,7 +32,7 @@ public class FilterWithin extends AbstractFilterGeoPipe { private Geometry other; private Envelope otherEnvelope; - + public FilterWithin(Geometry other) { this.other = other; this.otherEnvelope = other.getEnvelopeInternal(); @@ -43,7 +42,7 @@ public FilterWithin(Geometry other) { protected boolean validate(GeoPipeFlow flow) { // check if every point of this geometry is a point of the other geometry, // and the interiors of the two geometries have at least one point in common - return otherEnvelope.contains(flow.getEnvelope()) + return otherEnvelope.contains(flow.getEnvelope()) && flow.getGeometry().within(other); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/AbstractPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/AbstractPipe.java index 65f44f98a..6ce47725e 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/AbstractPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/AbstractPipe.java @@ -15,107 +15,109 @@ * return e; * } * - * If the current incoming S is not to be emitted and there are no other S objects to process and emit, then throw a NoSuchElementException. + * If the current incoming S is not to be emitted and there are no other S objects to process and emit, then throw a + * NoSuchElementException. * * @author Marko A. Rodriguez (http://markorodriguez.com) */ public abstract class AbstractPipe implements Pipe { - protected Iterator starts; - private E nextEnd; - protected E currentEnd; - private boolean available = false; - - public void setStarts(final Pipe starts) { - this.starts = starts; - } - - public void setStarts(final Iterator starts) { - if (starts instanceof Pipe) - this.starts = starts; - else - this.starts = new LastElementIterator<>(starts); - } - - public void setStarts(final Iterable starts) { - this.setStarts(starts.iterator()); - } - - public void reset() { - if (this.starts instanceof Pipe) { - ((Pipe) this.starts).reset(); - } - if (this.starts instanceof LastElementIterator) { - ((LastElementIterator) this.starts).reset(); - } - this.nextEnd = null; - this.currentEnd = null; - this.available = false; - } - - public List getPath() { - final List pathElements = getPathToHere(); - final int size = pathElements.size(); - // do not repeat filters as they dup the object - if (size == 0 || pathElements.get(size - 1) != this.currentEnd) { - pathElements.add(this.currentEnd); - } - return pathElements; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - - public E next() { - if (this.available) { - this.available = false; - return (this.currentEnd = this.nextEnd); - } else { - return (this.currentEnd = this.processNextStart()); - } - } - - public boolean hasNext() { - if (this.available) - return true; - else { - try { - this.nextEnd = this.processNextStart(); - return (this.available = true); - } catch (final NoSuchElementException e) { - return (this.available = false); - } - } - } - - /** - * The iterator method of Iterable is not faithful to the Java semantics of iterator(). - * This method simply returns the pipe itself (which is an iterator) and thus, is useful only for foreach iteration. - * - * @return the pipe from the perspective of an iterator - */ - public Iterator iterator() { - return this; - } - - public String toString() { - return getClass().getSimpleName(); - } - - protected abstract E processNextStart() throws NoSuchElementException; - - protected List getPathToHere() { - if (this.starts instanceof Pipe) { - return ((Pipe) this.starts).getPath(); - } else if (this.starts instanceof LastElementIterator) { - final List list = new ArrayList<>(); - list.add((E) ((LastElementIterator) starts).lastElement()); - return list; - } else { - return new ArrayList<>(); - } - } + protected Iterator starts; + private E nextEnd; + protected E currentEnd; + private boolean available = false; + + public void setStarts(final Pipe starts) { + this.starts = starts; + } + + public void setStarts(final Iterator starts) { + if (starts instanceof Pipe) { + this.starts = starts; + } else { + this.starts = new LastElementIterator<>(starts); + } + } + + public void setStarts(final Iterable starts) { + this.setStarts(starts.iterator()); + } + + public void reset() { + if (this.starts instanceof Pipe) { + ((Pipe) this.starts).reset(); + } + if (this.starts instanceof LastElementIterator) { + ((LastElementIterator) this.starts).reset(); + } + this.nextEnd = null; + this.currentEnd = null; + this.available = false; + } + + public List getPath() { + final List pathElements = getPathToHere(); + final int size = pathElements.size(); + // do not repeat filters as they dup the object + if (size == 0 || pathElements.get(size - 1) != this.currentEnd) { + pathElements.add(this.currentEnd); + } + return pathElements; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public E next() { + if (this.available) { + this.available = false; + return (this.currentEnd = this.nextEnd); + } else { + return (this.currentEnd = this.processNextStart()); + } + } + + public boolean hasNext() { + if (this.available) { + return true; + } else { + try { + this.nextEnd = this.processNextStart(); + return (this.available = true); + } catch (final NoSuchElementException e) { + return (this.available = false); + } + } + } + + /** + * The iterator method of Iterable is not faithful to the Java semantics of iterator(). + * This method simply returns the pipe itself (which is an iterator) and thus, is useful only for foreach iteration. + * + * @return the pipe from the perspective of an iterator + */ + public Iterator iterator() { + return this; + } + + public String toString() { + return getClass().getSimpleName(); + } + + protected abstract E processNextStart() throws NoSuchElementException; + + protected List getPathToHere() { + if (this.starts instanceof Pipe) { + return ((Pipe) this.starts).getPath(); + } else if (this.starts instanceof LastElementIterator) { + final List list = new ArrayList<>(); + list.add((E) ((LastElementIterator) starts).lastElement()); + return list; + } else { + return new ArrayList<>(); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/FilterPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/FilterPipe.java index 8cbbc247b..1706174be 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/FilterPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/FilterPipe.java @@ -7,38 +7,45 @@ * @author Marko A. Rodriguez (http://markorodriguez.com) */ public interface FilterPipe extends Pipe { - enum Filter { - EQUAL, NOT_EQUAL, GREATER_THAN, LESS_THAN, GREATER_THAN_EQUAL, LESS_THAN_EQUAL; - public boolean compare(Object leftObject, Object rightObject) { - switch (this) { - case EQUAL: - if (null == leftObject) - return rightObject == null; - return leftObject.equals(rightObject); - case NOT_EQUAL: - if (null == leftObject) - return rightObject != null; - return !leftObject.equals(rightObject); - case GREATER_THAN: - if (null == leftObject || rightObject == null) - return false; - return ((Comparable) leftObject).compareTo(rightObject) == 1; - case LESS_THAN: - if (null == leftObject || rightObject == null) - return false; - return ((Comparable) leftObject).compareTo(rightObject) == -1; - case GREATER_THAN_EQUAL: - if (null == leftObject || rightObject == null) - return false; - return ((Comparable) leftObject).compareTo(rightObject) >= 0; - case LESS_THAN_EQUAL: - if (null == leftObject || rightObject == null) - return false; - return ((Comparable) leftObject).compareTo(rightObject) <= 0; - default: - throw new IllegalArgumentException("Invalid state as no valid filter was provided"); - } - } - } -} \ No newline at end of file + enum Filter { + EQUAL, NOT_EQUAL, GREATER_THAN, LESS_THAN, GREATER_THAN_EQUAL, LESS_THAN_EQUAL; + + public boolean compare(Object leftObject, Object rightObject) { + switch (this) { + case EQUAL: + if (null == leftObject) { + return rightObject == null; + } + return leftObject.equals(rightObject); + case NOT_EQUAL: + if (null == leftObject) { + return rightObject != null; + } + return !leftObject.equals(rightObject); + case GREATER_THAN: + if (null == leftObject || rightObject == null) { + return false; + } + return ((Comparable) leftObject).compareTo(rightObject) == 1; + case LESS_THAN: + if (null == leftObject || rightObject == null) { + return false; + } + return ((Comparable) leftObject).compareTo(rightObject) == -1; + case GREATER_THAN_EQUAL: + if (null == leftObject || rightObject == null) { + return false; + } + return ((Comparable) leftObject).compareTo(rightObject) >= 0; + case LESS_THAN_EQUAL: + if (null == leftObject || rightObject == null) { + return false; + } + return ((Comparable) leftObject).compareTo(rightObject) <= 0; + default: + throw new IllegalArgumentException("Invalid state as no valid filter was provided"); + } + } + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/IdentityPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/IdentityPipe.java index 02f69e6ed..083e91b6b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/IdentityPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/IdentityPipe.java @@ -14,7 +14,8 @@ * @author Marko A. Rodriguez (http://markorodriguez.com) */ public class IdentityPipe extends AbstractPipe { - protected S processNextStart() { - return this.starts.next(); - } + + protected S processNextStart() { + return this.starts.next(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/LastElementIterator.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/LastElementIterator.java index 8aa0e42c1..5cfe1ce4c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/LastElementIterator.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/LastElementIterator.java @@ -1,41 +1,40 @@ package org.neo4j.gis.spatial.pipes.impl; -import org.neo4j.gis.spatial.utilities.RelationshipTraversal; - import java.util.Iterator; +import org.neo4j.gis.spatial.utilities.RelationshipTraversal; public class LastElementIterator implements Iterator { - private final Iterator source; - private T lastElement; - - public LastElementIterator(final Iterator source) { - this.source = source; - } - - public boolean hasNext() { - return source.hasNext(); - } - - public T next() { - return lastElement = source.next(); - } - - public void remove() { - throw new UnsupportedOperationException("remove not supported"); - } - - public T lastElement() { - return lastElement; - } - - /** - * Work around bug in Neo4j 4.3 with leaked RelationshipTraversalCursor - */ - public void reset() { - // TODO: rather try get deeper into the underlying index and close resources instead of exhausting the iterator - // The challenge is that there are many sources, all of which need to be made closable, and that is very hard - // to achieve in a generic way without a full-stack code change. - RelationshipTraversal.exhaustIterator(source); - } + private final Iterator source; + private T lastElement; + + public LastElementIterator(final Iterator source) { + this.source = source; + } + + public boolean hasNext() { + return source.hasNext(); + } + + public T next() { + return lastElement = source.next(); + } + + public void remove() { + throw new UnsupportedOperationException("remove not supported"); + } + + public T lastElement() { + return lastElement; + } + + /** + * Work around bug in Neo4j 4.3 with leaked RelationshipTraversalCursor + */ + public void reset() { + // TODO: rather try get deeper into the underlying index and close resources instead of exhausting the iterator + // The challenge is that there are many sources, all of which need to be made closable, and that is very hard + // to achieve in a generic way without a full-stack code change. + RelationshipTraversal.exhaustIterator(source); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipe.java index e186f9562..44a4a8945 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipe.java @@ -15,34 +15,35 @@ */ public interface Pipe extends Iterator, Iterable { - /** - * Set an iterator of S objects to the head (start) of the pipe. - * - * @param starts the iterator of incoming objects - */ - public void setStarts(Iterator starts); + /** + * Set an iterator of S objects to the head (start) of the pipe. + * + * @param starts the iterator of incoming objects + */ + public void setStarts(Iterator starts); - /** - * Set an iterable of S objects to the head (start) of the pipe. - * - * @param starts the iterable of incoming objects - */ - public void setStarts(Iterable starts); + /** + * Set an iterable of S objects to the head (start) of the pipe. + * + * @param starts the iterable of incoming objects + */ + public void setStarts(Iterable starts); - /** - * Returns the transformation path to arrive at the current object of the pipe. - * - * @return a List of all of the objects traversed for the current iterator position of the pipe. - */ - public List getPath(); + /** + * Returns the transformation path to arrive at the current object of the pipe. + * + * @return a List of all of the objects traversed for the current iterator position of the pipe. + */ + public List getPath(); - /** - * A pipe may maintain state. Reset is used to remove state. - * The general use case for reset() is to reuse a pipe in another computation without having to create a new Pipe object. - */ - public void reset(); + /** + * A pipe may maintain state. Reset is used to remove state. + * The general use case for reset() is to reuse a pipe in another computation without having to create a new Pipe + * object. + */ + public void reset(); - public default Stream stream() { - return StreamSupport.stream(spliterator(),false); - } + public default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipeline.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipeline.java index bf5f869f6..8c57baea1 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipeline.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/Pipeline.java @@ -1,9 +1,13 @@ package org.neo4j.gis.spatial.pipes.impl; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; import org.neo4j.internal.helpers.collection.Iterators; -import java.util.*; - /** * A Pipeline is a linear composite of Pipes. * Pipeline takes a List of Pipes and joins them according to their order as specified by their location in the List. @@ -15,198 +19,203 @@ */ public class Pipeline implements Pipe { - protected Pipe startPipe; - protected Pipe endPipe; - protected List pipes; - protected Iterator starts; - - public Pipeline() { - this.pipes = new ArrayList(); - } - - /** - * Constructs a pipeline from the provided pipes. The ordered list determines how the pipes will be chained together. - * When the pipes are chained together, the start of pipe n is the end of pipe n-1. - * - * @param pipes the ordered list of pipes to chain together into a pipeline - */ - public Pipeline(final List pipes) { - this.pipes = pipes; - this.setPipes(pipes); - } - - - /** - * Constructs a pipeline from the provided pipes. The ordered array determines how the pipes will be chained together. - * When the pipes are chained together, the start of pipe n is the end of pipe n-1. - * - * @param pipes the ordered array of pipes to chain together into a pipeline - */ - public Pipeline(final Pipe... pipes) { - this(new ArrayList(Arrays.asList(pipes))); - } - - /** - * Useful for constructing the pipeline chain without making use of the constructor. - * - * @param pipes the ordered list of pipes to chain together into a pipeline - */ - protected void setPipes(final List pipes) { - this.startPipe = (Pipe) pipes.get(0); - this.endPipe = (Pipe) pipes.get(pipes.size() - 1); - for (int i = 1; i < pipes.size(); i++) { - pipes.get(i).setStarts((Iterator) pipes.get(i - 1)); - } - } - - /** - * Useful for constructing the pipeline chain without making use of the constructor. - * - * @param pipes the ordered array of pipes to chain together into a pipeline - */ - protected void setPipes(final Pipe... pipes) { - this.setPipes(Arrays.asList(pipes)); - } - - /** - * Adds a new pipe to the end of the pipeline and then reconstructs the pipeline chain. - * - * @param pipe the new pipe to add to the pipeline - */ - public void addPipe(final Pipe pipe) { - this.pipes.add(pipe); - this.setPipes(this.pipes); - } - - public void setStarts(final Iterator starts) { - this.starts = starts; - this.startPipe.setStarts(starts); - } - - public void setStarts(final Iterable starts) { - this.setStarts(starts.iterator()); - } - - /** - * An unsupported operation that throws an UnsupportedOperationException. - */ - public void remove() { - throw new UnsupportedOperationException(); - } - - /** - * Determines if there is another object that can be emitted from the pipeline. - * - * @return true if an object can be next()'d out of the pipeline - */ - public boolean hasNext() { - return this.endPipe.hasNext(); - } - - /** - * Get the next object emitted from the pipeline. - * If no such object exists, then a NoSuchElementException is thrown. - * - * @return the next emitted object - */ - public E next() { - return this.endPipe.next(); - } - - public List getPath() { - return this.endPipe.getPath(); - } - - /** - * Get the number of pipes in the pipeline. - * - * @return the pipeline length - */ - public int size() { - return this.pipes.size(); - } - - public void reset() { - this.startPipe.reset(); // Clear incoming state to avoid bug in Neo4j 4.3 with leaked RelationshipTraversalCursor - this.endPipe.reset(); - } - - /** - * Simply returns this as as a pipeline (more specifically, pipe) implements Iterator. - * - * @return returns the iterator representation of this pipeline - */ - public Iterator iterator() { - return this; - } - - public String toString() { - return this.pipes.toString(); - } - - public List getPipes() { - return this.pipes; - } - - public Iterator getStarts() { - return this.starts; - } - - public Pipe remove(final int index) { - return this.pipes.remove(index); - } - - public Pipe get(final int index) { - return this.pipes.get(index); - } - - public boolean equals(final Object object) { - return (object instanceof Pipeline) && areEqual(this, (Pipeline) object); - } - - public static boolean areEqual(final Iterator it1, final Iterator it2) { - if (it1.hasNext() != it2.hasNext()) - return false; - - while (it1.hasNext()) { - if (!it2.hasNext()) - return false; - if (it1.next() != it2.next()) - return false; - } - return true; - } - - - public long count() { - return Iterators.count((Iterator) this); - } - - public void iterate() { - try { - while (true) { - next(); - } - } catch (final NoSuchElementException e) { - } - } - - public List next(final int number) { - final List list = new ArrayList(number); - try { - for (int i = 0; i < number; i++) { - list.add(next()); - } - } catch (final NoSuchElementException e) { - } - return list; - } - - public List toList() { - return Iterators.addToCollection((Iterator) this,new ArrayList()); - } - - public Collection fill(final Collection collection) { - return Iterators.addToCollection((Iterator) this,collection); - } -} \ No newline at end of file + protected Pipe startPipe; + protected Pipe endPipe; + protected List pipes; + protected Iterator starts; + + public Pipeline() { + this.pipes = new ArrayList(); + } + + /** + * Constructs a pipeline from the provided pipes. The ordered list determines how the pipes will be chained + * together. + * When the pipes are chained together, the start of pipe n is the end of pipe n-1. + * + * @param pipes the ordered list of pipes to chain together into a pipeline + */ + public Pipeline(final List pipes) { + this.pipes = pipes; + this.setPipes(pipes); + } + + + /** + * Constructs a pipeline from the provided pipes. The ordered array determines how the pipes will be chained + * together. + * When the pipes are chained together, the start of pipe n is the end of pipe n-1. + * + * @param pipes the ordered array of pipes to chain together into a pipeline + */ + public Pipeline(final Pipe... pipes) { + this(new ArrayList(Arrays.asList(pipes))); + } + + /** + * Useful for constructing the pipeline chain without making use of the constructor. + * + * @param pipes the ordered list of pipes to chain together into a pipeline + */ + protected void setPipes(final List pipes) { + this.startPipe = (Pipe) pipes.get(0); + this.endPipe = (Pipe) pipes.get(pipes.size() - 1); + for (int i = 1; i < pipes.size(); i++) { + pipes.get(i).setStarts((Iterator) pipes.get(i - 1)); + } + } + + /** + * Useful for constructing the pipeline chain without making use of the constructor. + * + * @param pipes the ordered array of pipes to chain together into a pipeline + */ + protected void setPipes(final Pipe... pipes) { + this.setPipes(Arrays.asList(pipes)); + } + + /** + * Adds a new pipe to the end of the pipeline and then reconstructs the pipeline chain. + * + * @param pipe the new pipe to add to the pipeline + */ + public void addPipe(final Pipe pipe) { + this.pipes.add(pipe); + this.setPipes(this.pipes); + } + + public void setStarts(final Iterator starts) { + this.starts = starts; + this.startPipe.setStarts(starts); + } + + public void setStarts(final Iterable starts) { + this.setStarts(starts.iterator()); + } + + /** + * An unsupported operation that throws an UnsupportedOperationException. + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Determines if there is another object that can be emitted from the pipeline. + * + * @return true if an object can be next()'d out of the pipeline + */ + public boolean hasNext() { + return this.endPipe.hasNext(); + } + + /** + * Get the next object emitted from the pipeline. + * If no such object exists, then a NoSuchElementException is thrown. + * + * @return the next emitted object + */ + public E next() { + return this.endPipe.next(); + } + + public List getPath() { + return this.endPipe.getPath(); + } + + /** + * Get the number of pipes in the pipeline. + * + * @return the pipeline length + */ + public int size() { + return this.pipes.size(); + } + + public void reset() { + this.startPipe.reset(); // Clear incoming state to avoid bug in Neo4j 4.3 with leaked RelationshipTraversalCursor + this.endPipe.reset(); + } + + /** + * Simply returns this as as a pipeline (more specifically, pipe) implements Iterator. + * + * @return returns the iterator representation of this pipeline + */ + public Iterator iterator() { + return this; + } + + public String toString() { + return this.pipes.toString(); + } + + public List getPipes() { + return this.pipes; + } + + public Iterator getStarts() { + return this.starts; + } + + public Pipe remove(final int index) { + return this.pipes.remove(index); + } + + public Pipe get(final int index) { + return this.pipes.get(index); + } + + public boolean equals(final Object object) { + return (object instanceof Pipeline) && areEqual(this, (Pipeline) object); + } + + public static boolean areEqual(final Iterator it1, final Iterator it2) { + if (it1.hasNext() != it2.hasNext()) { + return false; + } + + while (it1.hasNext()) { + if (!it2.hasNext()) { + return false; + } + if (it1.next() != it2.next()) { + return false; + } + } + return true; + } + + + public long count() { + return Iterators.count((Iterator) this); + } + + public void iterate() { + try { + while (true) { + next(); + } + } catch (final NoSuchElementException e) { + } + } + + public List next(final int number) { + final List list = new ArrayList(number); + try { + for (int i = 0; i < number; i++) { + list.add(next()); + } + } catch (final NoSuchElementException e) { + } + return list; + } + + public List toList() { + return Iterators.addToCollection((Iterator) this, new ArrayList()); + } + + public Collection fill(final Collection collection) { + return Iterators.addToCollection((Iterator) this, collection); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/impl/RangeFilterPipe.java b/src/main/java/org/neo4j/gis/spatial/pipes/impl/RangeFilterPipe.java index 76a8295b2..0eaf7d3ea 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/impl/RangeFilterPipe.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/impl/RangeFilterPipe.java @@ -11,37 +11,37 @@ */ public class RangeFilterPipe extends AbstractPipe implements FilterPipe { - private final long low; - private final long high; - private int counter = -1; + private final long low; + private final long high; + private int counter = -1; - public RangeFilterPipe(final long low, final long high) { - this.low = low; - this.high = high; - if (this.low != -1 && this.high != -1 && this.low > this.high) { - throw new IllegalArgumentException("Not a legal range: [" + low + ", " + high + "]"); - } - } + public RangeFilterPipe(final long low, final long high) { + this.low = low; + this.high = high; + if (this.low != -1 && this.high != -1 && this.low > this.high) { + throw new IllegalArgumentException("Not a legal range: [" + low + ", " + high + "]"); + } + } - protected S processNextStart() { - while (true) { - final S s = this.starts.next(); - this.counter++; - if ((this.low == -1 || this.counter >= this.low) && (this.high == -1 || this.counter <= this.high)) { - return s; - } - if (this.high != -1 && this.counter > this.high) { - throw new NoSuchElementException(); - } - } - } + protected S processNextStart() { + while (true) { + final S s = this.starts.next(); + this.counter++; + if ((this.low == -1 || this.counter >= this.low) && (this.high == -1 || this.counter <= this.high)) { + return s; + } + if (this.high != -1 && this.counter > this.high) { + throw new NoSuchElementException(); + } + } + } - public String toString() { - return String.format("%s (%d, %d)",getClass().getSimpleName(),low,high); - } + public String toString() { + return String.format("%s (%d, %d)", getClass().getSimpleName(), low, high); + } - public void reset() { - this.counter = -1; - super.reset(); - } + public void reset() { + this.counter = -1; + super.reset(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/osm/OSMGeoPipeline.java b/src/main/java/org/neo4j/gis/spatial/pipes/osm/OSMGeoPipeline.java index 6feee22f5..1000a74f4 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/osm/OSMGeoPipeline.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/osm/OSMGeoPipeline.java @@ -31,37 +31,37 @@ import org.neo4j.graphdb.Transaction; public class OSMGeoPipeline extends GeoPipeline { - - protected OSMGeoPipeline(Layer layer) { + + protected OSMGeoPipeline(Layer layer) { super(layer); } - public static OSMGeoPipeline startOsm(Transaction tx, Layer layer, final SearchRecords records) { - OSMGeoPipeline pipeline = new OSMGeoPipeline(layer); - return (OSMGeoPipeline) pipeline.add(createStartPipe(records)); - } - - public static OSMGeoPipeline startOsm(Transaction tx, Layer layer, SearchFilter searchFilter) { - return startOsm(tx, layer, layer.getIndex().search(tx, searchFilter)); - } + public static OSMGeoPipeline startOsm(Transaction tx, Layer layer, final SearchRecords records) { + OSMGeoPipeline pipeline = new OSMGeoPipeline(layer); + return (OSMGeoPipeline) pipeline.add(createStartPipe(records)); + } + + public static OSMGeoPipeline startOsm(Transaction tx, Layer layer, SearchFilter searchFilter) { + return startOsm(tx, layer, layer.getIndex().search(tx, searchFilter)); + } - public static OSMGeoPipeline startOsm(Transaction tx, Layer layer) { - return startOsm(tx, layer, new SearchAll()); - } - - public OSMGeoPipeline addOsmPipe(AbstractGeoPipe geoPipe) { - return (OSMGeoPipeline) add(geoPipe); - } - - public OSMGeoPipeline extractOsmPoints() { - return addOsmPipe(new ExtractOSMPoints(layer.getGeometryFactory())); - } - - public OSMGeoPipeline osmAttributeFilter(String key, Object value) { - return addOsmPipe(new FilterOSMAttributes(key, value)); - } - - public OSMGeoPipeline osmAttributeFilter(String key, String value, FilterPipe.Filter comparison) { - return addOsmPipe(new FilterOSMAttributes(key, value, comparison)); - } -} \ No newline at end of file + public static OSMGeoPipeline startOsm(Transaction tx, Layer layer) { + return startOsm(tx, layer, new SearchAll()); + } + + public OSMGeoPipeline addOsmPipe(AbstractGeoPipe geoPipe) { + return (OSMGeoPipeline) add(geoPipe); + } + + public OSMGeoPipeline extractOsmPoints() { + return addOsmPipe(new ExtractOSMPoints(layer.getGeometryFactory())); + } + + public OSMGeoPipeline osmAttributeFilter(String key, Object value) { + return addOsmPipe(new FilterOSMAttributes(key, value)); + } + + public OSMGeoPipeline osmAttributeFilter(String key, String value, FilterPipe.Filter comparison) { + return addOsmPipe(new FilterOSMAttributes(key, value, comparison)); + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/osm/filtering/FilterOSMAttributes.java b/src/main/java/org/neo4j/gis/spatial/pipes/osm/filtering/FilterOSMAttributes.java index cb2bc28ee..793644509 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/osm/filtering/FilterOSMAttributes.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/osm/filtering/FilterOSMAttributes.java @@ -32,11 +32,11 @@ public class FilterOSMAttributes extends AbstractGeoPipe { private String key; private Object value; private FilterPipe.Filter comparison; - + public FilterOSMAttributes(String key, Object value) { this(key, value, FilterPipe.Filter.EQUAL); - } - + } + public FilterOSMAttributes(String key, Object value, FilterPipe.Filter comparison) { this.key = key; this.value = value; @@ -48,11 +48,11 @@ protected GeoPipeFlow process(GeoPipeFlow flow) { Node geomNode = flow.getRecord().getGeomNode(); Node waysNode = geomNode.getSingleRelationship(OSMRelation.GEOM, Direction.INCOMING).getStartNode(); Node tagNode = waysNode.getSingleRelationship(OSMRelation.TAGS, Direction.OUTGOING).getEndNode(); - if (tagNode.hasProperty(key) + if (tagNode.hasProperty(key) && comparison.compare(tagNode.getProperty(key), value)) { return flow; } else { return null; } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/osm/processing/ExtractOSMPoints.java b/src/main/java/org/neo4j/gis/spatial/pipes/osm/processing/ExtractOSMPoints.java index 5b7615c7f..4c79c5bbb 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/osm/processing/ExtractOSMPoints.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/osm/processing/ExtractOSMPoints.java @@ -21,55 +21,56 @@ import static org.neo4j.gis.spatial.utilities.TraverserFactory.createTraverserInBackwardsCompatibleWay; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.osm.OSMRelation; import org.neo4j.gis.spatial.pipes.AbstractExtractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; import org.neo4j.graphdb.impl.OrderedByTypeExpander; import org.neo4j.graphdb.traversal.Evaluation; import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.graphdb.traversal.Uniqueness; - -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; public class ExtractOSMPoints extends AbstractExtractGeoPipe { - private GeometryFactory geomFactory; + private GeometryFactory geomFactory; - public ExtractOSMPoints(GeometryFactory geomFactory) { - this.geomFactory = geomFactory; - } + public ExtractOSMPoints(GeometryFactory geomFactory) { + this.geomFactory = geomFactory; + } - @Override - protected void extract(GeoPipeFlow pipeFlow) { - Node geomNode = pipeFlow.getRecord().getGeomNode(); - Node node = geomNode.getSingleRelationship(OSMRelation.GEOM, Direction.INCOMING).getStartNode(); + @Override + protected void extract(GeoPipeFlow pipeFlow) { + Node geomNode = pipeFlow.getRecord().getGeomNode(); + Node node = geomNode.getSingleRelationship(OSMRelation.GEOM, Direction.INCOMING).getStartNode(); - TraversalDescription td = new MonoDirectionalTraversalDescription().evaluator(path -> { - if (path.length() > 0 - && !path.relationships().iterator().next().isType(OSMRelation.NEXT) - && path.lastRelationship().isType(OSMRelation.NODE)) { - return Evaluation.INCLUDE_AND_PRUNE; - } + TraversalDescription td = new MonoDirectionalTraversalDescription().evaluator(path -> { + if (path.length() > 0 + && !path.relationships().iterator().next().isType(OSMRelation.NEXT) + && path.lastRelationship().isType(OSMRelation.NODE)) { + return Evaluation.INCLUDE_AND_PRUNE; + } - return Evaluation.EXCLUDE_AND_CONTINUE; - }).expand(new OrderedByTypeExpander() - .add(OSMRelation.FIRST_NODE, Direction.OUTGOING) - .add(OSMRelation.NEXT, Direction.OUTGOING) - .add(OSMRelation.NODE, Direction.OUTGOING)) - .uniqueness(Uniqueness.NODE_PATH); + return Evaluation.EXCLUDE_AND_CONTINUE; + }).expand(new OrderedByTypeExpander() + .add(OSMRelation.FIRST_NODE, Direction.OUTGOING) + .add(OSMRelation.NEXT, Direction.OUTGOING) + .add(OSMRelation.NODE, Direction.OUTGOING)) + .uniqueness(Uniqueness.NODE_PATH); - int counter = 0; - for (Path path : createTraverserInBackwardsCompatibleWay(td, node)) { - Node pointNode = path.endNode(); - double longitude = (Double) pointNode.getProperty("lon"); - double latitude = (Double) pointNode.getProperty("lat"); + int counter = 0; + for (Path path : createTraverserInBackwardsCompatibleWay(td, node)) { + Node pointNode = path.endNode(); + double longitude = (Double) pointNode.getProperty("lon"); + double latitude = (Double) pointNode.getProperty("lat"); - GeoPipeFlow newPoint = pipeFlow.makeClone("osmpoint" + counter++); - newPoint.setGeometry(geomFactory.createPoint(new Coordinate(longitude, latitude))); - extracts.add(newPoint); - } - } + GeoPipeFlow newPoint = pipeFlow.makeClone("osmpoint" + counter++); + newPoint.setGeometry(geomFactory.createPoint(new Coordinate(longitude, latitude))); + extracts.add(newPoint); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ApplyAffineTransformation.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ApplyAffineTransformation.java index c0cf8ee42..2980fa9fc 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ApplyAffineTransformation.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ApplyAffineTransformation.java @@ -19,39 +19,38 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.util.AffineTransformation; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.util.AffineTransformation; - /** * Applies an Affine Transformation to every geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class ApplyAffineTransformation extends AbstractGeoPipe { - + private AffineTransformation t; - + /** * @param t affine transformation */ public ApplyAffineTransformation(AffineTransformation t) { this.t = t; - } + } /** - * @param t affine transformation + * @param t affine transformation * @param resultPropertyName property name to use for geometry output */ public ApplyAffineTransformation(AffineTransformation t, String resultPropertyName) { super(resultPropertyName); this.t = t; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, t.transform(flow.getGeometry())); return flow; - } - + } + } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Area.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Area.java index 3d4a7b75d..9c7a9e5d2 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Area.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Area.java @@ -29,19 +29,19 @@ public class Area extends AbstractGeoPipe { public Area() { - } + } /** * @param resultPropertyName property name to use for output */ public Area(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getArea()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Boundary.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Boundary.java index 3d6457827..b63a23dc9 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Boundary.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Boundary.java @@ -30,20 +30,20 @@ public class Boundary extends AbstractGeoPipe { public Boundary() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Boundary(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { // TODO convert Linear Ring to ... Line? - + setGeometry(flow, flow.getGeometry().getBoundary()); return flow; - } -} \ No newline at end of file + } +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Buffer.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Buffer.java index 4d7f5c7c2..2f339352a 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Buffer.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Buffer.java @@ -27,7 +27,7 @@ * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Buffer extends AbstractGeoPipe { - + private double distance; /** @@ -35,16 +35,16 @@ public class Buffer extends AbstractGeoPipe { */ public Buffer(double distance) { this.distance = distance; - } - + } + /** - * @param distance buffer size + * @param distance buffer size * @param resultPropertyName property name to use for geometry output - */ + */ public Buffer(double distance, String resultPropertyName) { super(resultPropertyName); this.distance = distance; - } + } @Override protected GeoPipeFlow process(GeoPipeFlow flow) { diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Centroid.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Centroid.java index 7a31c3c43..ab0287e44 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Centroid.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Centroid.java @@ -28,21 +28,21 @@ * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Centroid extends AbstractGeoPipe { - + public Centroid() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Centroid(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, flow.getGeometry().getCentroid()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ConvexHull.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ConvexHull.java index 850b4da89..970989c37 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ConvexHull.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ConvexHull.java @@ -27,20 +27,20 @@ * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class ConvexHull extends AbstractGeoPipe { - + public ConvexHull() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public ConvexHull(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, flow.getGeometry().convexHull()); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/CopyDatabaseRecordProperties.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/CopyDatabaseRecordProperties.java index 0313040b6..6a59b015b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/CopyDatabaseRecordProperties.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/CopyDatabaseRecordProperties.java @@ -32,32 +32,32 @@ */ public class CopyDatabaseRecordProperties extends AbstractGeoPipe { - private final String[] keys; - private final Transaction tx; - - public CopyDatabaseRecordProperties(Transaction tx) { - this.tx = tx; - this.keys = null; - } - - public CopyDatabaseRecordProperties(Transaction tx, String key) { - this.tx = tx; - this.keys = new String[]{key}; - } - - public CopyDatabaseRecordProperties(Transaction tx, String[] keys) { - this.tx = tx; - this.keys = keys; - } - - @Override - protected GeoPipeFlow process(GeoPipeFlow flow) { - String[] names = keys != null ? keys : flow.getRecord().getPropertyNames(tx); - for (String name : names) { - flow.getProperties().put(name, flow.getRecord().getProperty(tx, name)); - } - - return flow; - } - -} \ No newline at end of file + private final String[] keys; + private final Transaction tx; + + public CopyDatabaseRecordProperties(Transaction tx) { + this.tx = tx; + this.keys = null; + } + + public CopyDatabaseRecordProperties(Transaction tx, String key) { + this.tx = tx; + this.keys = new String[]{key}; + } + + public CopyDatabaseRecordProperties(Transaction tx, String[] keys) { + this.tx = tx; + this.keys = keys; + } + + @Override + protected GeoPipeFlow process(GeoPipeFlow flow) { + String[] names = keys != null ? keys : flow.getRecord().getPropertyNames(tx); + for (String name : names) { + flow.getProperties().put(name, flow.getRecord().getProperty(tx, name)); + } + + return flow; + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Densify.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Densify.java index d73902caa..57da6ecfc 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Densify.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Densify.java @@ -19,13 +19,12 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.densify.Densifier; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.densify.Densifier; - /** - * Densify geometries by inserting extra vertices along the line segments in the geometry. + * Densify geometries by inserting extra vertices along the line segments in the geometry. * The densified geometry contains no line segment which is longer than the given distance tolerance. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ @@ -38,8 +37,8 @@ public class Densify extends AbstractGeoPipe { */ public Densify(double distanceTolerance) { this.distanceTolerance = distanceTolerance; - } - + } + /** * @param distanceTolerance * @param resultPropertyName property name to use for geometry output @@ -47,12 +46,12 @@ public Densify(double distanceTolerance) { public Densify(double distanceTolerance, String resultPropertyName) { super(resultPropertyName); this.distanceTolerance = distanceTolerance; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, Densifier.densify(flow.getGeometry(), distanceTolerance)); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/DensityIslands.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/DensityIslands.java index b3d605f53..531bfb2b8 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/DensityIslands.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/DensityIslands.java @@ -28,7 +28,6 @@ public class DensityIslands extends AbstractGroupGeoPipe { private double density; /** - * * @param density */ public DensityIslands(double density) { @@ -48,9 +47,9 @@ protected void group(GeoPipeFlow pipeFlow) { islandFound = true; } } - + if (!islandFound) { groups.add(pipeFlow); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Difference.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Difference.java index 1c0ebeeab..c10f1913b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Difference.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Difference.java @@ -19,35 +19,34 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Computes a geometry representing the points making up item geometry that do not make up the given geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Difference extends AbstractGeoPipe { - + private Geometry other; - + public Difference(Geometry other) { this.other = other; - } - + } + /** - * @param other geometry + * @param other geometry * @param resultPropertyName property name to use for geometry output - */ + */ public Difference(Geometry other, String resultPropertyName) { super(resultPropertyName); this.other = other; - } - - @Override + } + + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { - setGeometry(flow, flow.getGeometry().difference(other)); + setGeometry(flow, flow.getGeometry().difference(other)); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Dimension.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Dimension.java index 8504efcee..324dc40de 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Dimension.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Dimension.java @@ -29,19 +29,19 @@ public class Dimension extends AbstractGeoPipe { public Dimension() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Dimension(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getDimension()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Distance.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Distance.java index b0dd4e84d..d5fcb9ab2 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Distance.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Distance.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Calculates distance between the given geometry and item geometry for each item in the pipeline. @@ -31,23 +30,23 @@ public class Distance extends AbstractGeoPipe { private Geometry reference; - + public Distance(Geometry reference) { this.reference = reference; - } + } /** * @param resultPropertyName property name to use for geometry output - */ + */ public Distance(Geometry reference, String resultPropertyName) { super(resultPropertyName); this.reference = reference; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().distance(reference)); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/EndPoint.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/EndPoint.java index a2f6e6e83..c30223dab 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/EndPoint.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/EndPoint.java @@ -19,37 +19,36 @@ */ package org.neo4j.gis.spatial.pipes.processing; -import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; - import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; +import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** * Find the ending point of item geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class EndPoint extends AbstractGeoPipe { - + private GeometryFactory geomFactory; - + public EndPoint(GeometryFactory geomFactory) { this.geomFactory = geomFactory; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public EndPoint(GeometryFactory geomFactory, String resultPropertyName) { super(resultPropertyName); this.geomFactory = geomFactory; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { Coordinate[] coords = flow.getGeometry().getCoordinates(); setGeometry(flow, geomFactory.createPoint(coords[coords.length - 1])); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Envelope.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Envelope.java index 627085156..d76942b09 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Envelope.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Envelope.java @@ -27,20 +27,20 @@ * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Envelope extends AbstractGeoPipe { - + public Envelope() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Envelope(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, flow.getGeometry().getEnvelope()); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractGeometries.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractGeometries.java index af99e2549..71f3a7bd4 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractGeometries.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractGeometries.java @@ -26,7 +26,7 @@ * Extracts every geometry contained in an item multi geometry. */ public class ExtractGeometries extends AbstractExtractGeoPipe { - + @Override protected void extract(GeoPipeFlow pipeFlow) { if (pipeFlow.getGeometry().getNumGeometries() == 1) { diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractPoints.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractPoints.java index 95693c944..2de10281c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractPoints.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/ExtractPoints.java @@ -28,19 +28,19 @@ */ public class ExtractPoints extends AbstractExtractGeoPipe { - private GeometryFactory geomFactory; + private GeometryFactory geomFactory; - public ExtractPoints(GeometryFactory geomFactory) { - this.geomFactory = geomFactory; - } + public ExtractPoints(GeometryFactory geomFactory) { + this.geomFactory = geomFactory; + } - @Override - protected void extract(GeoPipeFlow pipeFlow) { - int numPoints = pipeFlow.getGeometry().getCoordinates().length; - for (int i = 0; i < numPoints; i++) { - GeoPipeFlow newPoint = pipeFlow.makeClone("point" + i); - newPoint.setGeometry(geomFactory.createPoint(pipeFlow.getGeometry().getCoordinates()[i])); - extracts.add(newPoint); - } - } + @Override + protected void extract(GeoPipeFlow pipeFlow) { + int numPoints = pipeFlow.getGeometry().getCoordinates().length; + for (int i = 0; i < numPoints; i++) { + GeoPipeFlow newPoint = pipeFlow.makeClone("point" + i); + newPoint.setGeometry(geomFactory.createPoint(pipeFlow.getGeometry().getCoordinates()[i])); + extracts.add(newPoint); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GML.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GML.java index a117671af..ee0e05e62 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GML.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GML.java @@ -19,32 +19,31 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.io.gml2.GMLWriter; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.io.gml2.GMLWriter; - /** * Encodes item geometry to GML. */ public class GML extends AbstractGeoPipe { - public GML() { - } + public GML() { + } - /** - * @param resultPropertyName property name to use for geometry output - */ - public GML(String resultPropertyName) { - super(resultPropertyName); - } + /** + * @param resultPropertyName property name to use for geometry output + */ + public GML(String resultPropertyName) { + super(resultPropertyName); + } - @Override - protected GeoPipeFlow process(GeoPipeFlow flow) { - GMLWriter gmlWriter = new GMLWriter(); - setProperty(flow, gmlWriter.write(flow.getGeometry())); - return flow; - } + @Override + protected GeoPipeFlow process(GeoPipeFlow flow) { + GMLWriter gmlWriter = new GMLWriter(); + setProperty(flow, gmlWriter.write(flow.getGeometry())); + return flow; + } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeoJSON.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeoJSON.java index 040b51844..7b991fbad 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeoJSON.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeoJSON.java @@ -30,20 +30,20 @@ public class GeoJSON extends AbstractGeoPipe { public GeoJSON() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public GeoJSON(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { GeometryJSON json = new GeometryJSON(); setProperty(flow, json.toString(flow.getGeometry())); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeometryType.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeometryType.java index 86773fa3e..61d882df8 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeometryType.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/GeometryType.java @@ -29,19 +29,19 @@ public class GeometryType extends AbstractGeoPipe { public GeometryType() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public GeometryType(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getGeometryType()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/InteriorPoint.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/InteriorPoint.java index e370b24b3..3dd8566f4 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/InteriorPoint.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/InteriorPoint.java @@ -23,26 +23,26 @@ import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** - * Computes an interior point of a geometry. - * An interior point is guaranteed to lie in the interior of the Geometry, if it possible. + * Computes an interior point of a geometry. + * An interior point is guaranteed to lie in the interior of the Geometry, if it possible. * Otherwise, the point may lie on the boundary of the geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class InteriorPoint extends AbstractGeoPipe { - + public InteriorPoint() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public InteriorPoint(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, flow.getGeometry().getInteriorPoint()); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/IntersectAll.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/IntersectAll.java index 5d99577ac..65ebd52e1 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/IntersectAll.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/IntersectAll.java @@ -29,7 +29,7 @@ */ public class IntersectAll extends AbstractGroupGeoPipe { - @Override + @Override protected void group(GeoPipeFlow flow) { if (groups.size() == 0) { groups.add(flow); @@ -39,4 +39,4 @@ protected void group(GeoPipeFlow flow) { result.merge(flow); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Intersection.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Intersection.java index aa479c391..e11f96316 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Intersection.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Intersection.java @@ -19,22 +19,21 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Computes a geometry representing the intersection between item geometry and the given geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Intersection extends AbstractGeoPipe { - + private Geometry other; - + public Intersection(Geometry other) { this.other = other; - } + } /** * @param resultPropertyName property name to use for geometry output @@ -42,11 +41,11 @@ public Intersection(Geometry other) { public Intersection(Geometry other, String resultPropertyName) { super(resultPropertyName); this.other = other; - } - - @Override + } + + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { - setGeometry(flow, flow.getGeometry().intersection(other)); + setGeometry(flow, flow.getGeometry().intersection(other)); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/KeyholeMarkupLanguage.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/KeyholeMarkupLanguage.java index 09b1c4e8b..9f4652545 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/KeyholeMarkupLanguage.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/KeyholeMarkupLanguage.java @@ -20,7 +20,6 @@ package org.neo4j.gis.spatial.pipes.processing; import java.io.IOException; - import org.geotools.kml.KML; import org.geotools.kml.KMLConfiguration; import org.geotools.xsd.Encoder; @@ -33,16 +32,16 @@ public class KeyholeMarkupLanguage extends AbstractGeoPipe { public KeyholeMarkupLanguage() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public KeyholeMarkupLanguage(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { Encoder encoder = new Encoder(new KMLConfiguration()); encoder.setIndenting(true); @@ -50,8 +49,8 @@ protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, encoder.encodeAsString(flow.getGeometry(), KML.Geometry)); } catch (IOException e) { setProperty(flow, e.getMessage()); - } + } return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Length.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Length.java index fe1ac7d81..ebb5925df 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Length.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Length.java @@ -29,19 +29,19 @@ public class Length extends AbstractGeoPipe { public Length() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Length(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getLength()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Max.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Max.java index eca40384b..1232bbe4c 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Max.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Max.java @@ -20,7 +20,6 @@ package org.neo4j.gis.spatial.pipes.processing; import java.util.Comparator; - import org.neo4j.gis.spatial.pipes.AbstractGroupGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; @@ -32,34 +31,34 @@ public class Max extends AbstractGroupGeoPipe { private String property; private Comparator comparator; - + public Max(String property, Comparator comparator) { this.property = property; this.comparator = comparator; } - + public Max(String property) { - this.property = property; + this.property = property; } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override protected void group(GeoPipeFlow flow) { if (flow.getProperties().get(property) == null) { return; } - + if (groups.size() == 0) { groups.add(flow); } else { Object min = groups.get(0).getProperties().get(property); Object other = flow.getProperties().get(property); - + int comparison; if (comparator == null) { comparison = ((Comparable) other).compareTo(min); } else { - comparison = comparator.compare(other, min); + comparison = comparator.compare(other, min); } if (comparison > 0) { @@ -71,4 +70,4 @@ protected void group(GeoPipeFlow flow) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Min.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Min.java index 7b0b079d7..8deb07549 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Min.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Min.java @@ -20,7 +20,6 @@ package org.neo4j.gis.spatial.pipes.processing; import java.util.Comparator; - import org.neo4j.gis.spatial.pipes.AbstractGroupGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; @@ -32,34 +31,34 @@ public class Min extends AbstractGroupGeoPipe { private String property; private Comparator comparator; - + public Min(String property, Comparator comparator) { this.property = property; this.comparator = comparator; } - + public Min(String property) { - this.property = property; + this.property = property; } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override protected void group(GeoPipeFlow flow) { if (flow.getProperties().get(property) == null) { return; } - + if (groups.size() == 0) { groups.add(flow); } else { Object min = groups.get(0).getProperties().get(property); Object other = flow.getProperties().get(property); - + int comparison; if (comparator == null) { comparison = ((Comparable) other).compareTo(min); } else { - comparison = comparator.compare(other, min); + comparison = comparator.compare(other, min); } if (comparison < 0) { @@ -71,4 +70,4 @@ protected void group(GeoPipeFlow flow) { } } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumGeometries.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumGeometries.java index 5eda9c2b4..75338982f 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumGeometries.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumGeometries.java @@ -29,19 +29,19 @@ public class NumGeometries extends AbstractGeoPipe { public NumGeometries() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public NumGeometries(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getNumGeometries()); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumPoints.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumPoints.java index 402d6e6ea..a3592f00b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumPoints.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/NumPoints.java @@ -30,17 +30,17 @@ public class NumPoints extends AbstractGeoPipe { public NumPoints() { } - + /** * @param resultPropertyName property name to use for geometry output - */ + */ public NumPoints(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, flow.getGeometry().getNumPoints()); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicDistance.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicDistance.java index b9fb7c23a..322642817 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicDistance.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicDistance.java @@ -19,30 +19,29 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Point; import org.locationtech.jts.operation.distance.DistanceOp; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Envelope; - /** * Calculates distance between the given geometry and item geometry for each item in the pipeline. * This pipe assume Layer contains geometries with Latitude / Longitude coordinates in degrees. - * + *

* Algorithm reference: http://www.movable-type.co.uk/scripts/latlong-db.html */ public class OrthodromicDistance extends AbstractGeoPipe { private Coordinate reference; public static final double earthRadiusInKm = 6371; - public static final String DISTANCE = "OrthodromicDistance"; + public static final String DISTANCE = "OrthodromicDistance"; public OrthodromicDistance(Coordinate reference) { - this(reference, OrthodromicDistance.DISTANCE); + this(reference, OrthodromicDistance.DISTANCE); } /** @@ -55,23 +54,23 @@ public OrthodromicDistance(Coordinate reference, String resultPropertyName) { @Override protected GeoPipeFlow process(GeoPipeFlow flow) { - double distanceInKm = calculateDistanceToGeometry(reference, flow.getGeometry()); + double distanceInKm = calculateDistanceToGeometry(reference, flow.getGeometry()); setProperty(flow, distanceInKm); return flow; } - public static double calculateDistanceToGeometry(Coordinate reference, Geometry geometry) { - if (geometry instanceof Point) { - Point point = (Point) geometry; - return calculateDistance(reference, point.getCoordinate()); - } else { - Geometry referencePoint = geometry.getFactory().createPoint(reference); - DistanceOp ops = new DistanceOp(referencePoint, geometry); - Coordinate[] nearest = ops.nearestPoints(); - assert nearest.length == 2; - return calculateDistance(nearest[0], nearest[1]); - } - } + public static double calculateDistanceToGeometry(Coordinate reference, Geometry geometry) { + if (geometry instanceof Point) { + Point point = (Point) geometry; + return calculateDistance(reference, point.getCoordinate()); + } else { + Geometry referencePoint = geometry.getFactory().createPoint(reference); + DistanceOp ops = new DistanceOp(referencePoint, geometry); + Coordinate[] nearest = ops.nearestPoints(); + assert nearest.length == 2; + return calculateDistance(nearest[0], nearest[1]); + } + } public static Envelope suggestSearchWindow(Coordinate reference, double maxDistanceInKm) { double lat = reference.y; @@ -90,10 +89,11 @@ public static double calculateDistance(Coordinate reference, Coordinate point) { // TODO use org.geotools.referencing.GeodeticCalculator? // d = acos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon2 - lon1)) * R - double distanceInKm = Math.acos(Math.min(Math.sin(Math.toRadians(reference.y)) * Math.sin(Math.toRadians(point.y)) - + Math.cos(Math.toRadians(reference.y)) * Math.cos(Math.toRadians(point.y)) - * Math.cos(Math.toRadians(point.x) - Math.toRadians(reference.x)),1.0)) - * earthRadiusInKm; + double distanceInKm = + Math.acos(Math.min(Math.sin(Math.toRadians(reference.y)) * Math.sin(Math.toRadians(point.y)) + + Math.cos(Math.toRadians(reference.y)) * Math.cos(Math.toRadians(point.y)) + * Math.cos(Math.toRadians(point.x) - Math.toRadians(reference.x)), 1.0)) + * earthRadiusInKm; return distanceInKm; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicLength.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicLength.java index d84ff245a..643ce96f6 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicLength.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/OrthodromicLength.java @@ -19,58 +19,57 @@ */ package org.neo4j.gis.spatial.pipes.processing; -import org.geotools.referencing.GeodeticCalculator; -import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; -import org.neo4j.gis.spatial.pipes.GeoPipeFlow; import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - +import org.geotools.referencing.GeodeticCalculator; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; +import org.neo4j.gis.spatial.pipes.GeoPipeFlow; /** * Calculates geometry length for each item in the pipeline. - * This pipe assume Layer contains geometries with Latitude / Longitude coordinates in degrees. + * This pipe assume Layer contains geometries with Latitude / Longitude coordinates in degrees. */ public class OrthodromicLength extends AbstractGeoPipe { protected CoordinateReferenceSystem crs; - + public OrthodromicLength(CoordinateReferenceSystem crs) { this.crs = crs; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public OrthodromicLength(CoordinateReferenceSystem crs, String resultPropertyName) { super(resultPropertyName); this.crs = crs; - } + } - @Override - protected GeoPipeFlow process(GeoPipeFlow flow) { + @Override + protected GeoPipeFlow process(GeoPipeFlow flow) { setProperty(flow, calculateLength(flow.getGeometry(), crs)); return flow; } - + protected double calculateLength(Geometry geometry, CoordinateReferenceSystem crs) { GeodeticCalculator geodeticCalculator = new GeodeticCalculator(crs); - + Coordinate[] coords = geometry.getCoordinates(); double totalLength = 0; - // accumulate the orthodromic distance for every point relation of the given geometry. + // accumulate the orthodromic distance for every point relation of the given geometry. for (int i = 0; i < (coords.length - 1); i++) { Coordinate c1 = coords[i]; Coordinate c2 = coords[i + 1]; geodeticCalculator.setStartingGeographicPoint(c1.x, c1.y); geodeticCalculator.setDestinationGeographicPoint(c2.x, c2.y); totalLength += geodeticCalculator.getOrthodromicDistance(); - } - + } + return totalLength; } - + } diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyPreservingTopology.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyPreservingTopology.java index 51ce81608..7590bebdc 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyPreservingTopology.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyPreservingTopology.java @@ -19,32 +19,31 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.simplify.TopologyPreservingSimplifier; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.simplify.TopologyPreservingSimplifier; - /** * Simplifies geometry for every item in the pipeline, using an algorithm that preserves geometry topology. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class SimplifyPreservingTopology extends AbstractGeoPipe { - + private double distanceTolerance; - + public SimplifyPreservingTopology(double distanceTolerance) { this.distanceTolerance = distanceTolerance; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public SimplifyPreservingTopology(double distanceTolerance, String resultPropertyName) { super(resultPropertyName); this.distanceTolerance = distanceTolerance; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, TopologyPreservingSimplifier.simplify(flow.getGeometry(), distanceTolerance)); return flow; diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyWithDouglasPeucker.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyWithDouglasPeucker.java index 0892fe0f7..2c52a62d8 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyWithDouglasPeucker.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SimplifyWithDouglasPeucker.java @@ -19,33 +19,32 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; - /** * Simplifies geometry for every item in the pipeline, using Douglas Peucker algorithm. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class SimplifyWithDouglasPeucker extends AbstractGeoPipe { - + private double distanceTolerance; - + public SimplifyWithDouglasPeucker(double distanceTolerance) { this.distanceTolerance = distanceTolerance; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public SimplifyWithDouglasPeucker(double distanceTolerance, String resultPropertyName) { super(resultPropertyName); this.distanceTolerance = distanceTolerance; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, DouglasPeuckerSimplifier.simplify(flow.getGeometry(), distanceTolerance)); return flow; diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Sort.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Sort.java index 86cb1a4d3..1470b55a3 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Sort.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Sort.java @@ -25,7 +25,6 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; - import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; @@ -37,9 +36,9 @@ public class Sort extends AbstractGeoPipe { private final List sortedFlow; private final Comparator comparator; private Iterator flowIterator; - + public Sort(final String property, final Comparator propertyComparator) { - this.sortedFlow = new ArrayList<>(); + this.sortedFlow = new ArrayList<>(); this.comparator = (o1, o2) -> { Object p1 = o1.getProperties().get(property); Object p2 = o2.getProperties().get(property); @@ -55,7 +54,7 @@ public Sort(final String property, final Comparator propertyComparator) } }; } - + public Sort(String property, final boolean asc) { this(property, (o1, o2) -> { int result = ((Comparable) o1).compareTo(o2); @@ -65,7 +64,7 @@ public Sort(String property, final boolean asc) { return result; }); } - + @Override public GeoPipeFlow processNextStart() { if (flowIterator == null) { @@ -74,13 +73,13 @@ public GeoPipeFlow processNextStart() { sortedFlow.add(starts.next()); } } catch (NoSuchElementException e) { - } - + } + Collections.sort(sortedFlow, comparator); flowIterator = sortedFlow.iterator(); } - + return flowIterator.next(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/StartPoint.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/StartPoint.java index d0f1516f5..7d62a349b 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/StartPoint.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/StartPoint.java @@ -19,35 +19,34 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.GeometryFactory; - /** * Find the starting point of item geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class StartPoint extends AbstractGeoPipe { - + private GeometryFactory geomFactory; - + public StartPoint(GeometryFactory geomFactory) { this.geomFactory = geomFactory; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public StartPoint(GeometryFactory geomFactory, String resultPropertyName) { super(resultPropertyName); this.geomFactory = geomFactory; - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { setGeometry(flow, geomFactory.createPoint(flow.getGeometry().getCoordinates()[0])); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SymDifference.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SymDifference.java index 7bf134838..16d74a093 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/SymDifference.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/SymDifference.java @@ -19,34 +19,33 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Computes the symmetric difference of the given geometry with item geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class SymDifference extends AbstractGeoPipe { - + private Geometry other; - + public SymDifference(Geometry other) { this.other = other; - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public SymDifference(Geometry other, String resultPropertyName) { super(resultPropertyName); this.other = other; - } - - @Override + } + + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { - setGeometry(flow, flow.getGeometry().symDifference(other)); + setGeometry(flow, flow.getGeometry().symDifference(other)); return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Union.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Union.java index 8546fef1a..890770b4e 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/Union.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/Union.java @@ -19,45 +19,44 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.geom.Geometry; - /** * Unites item geometry with itself or with the given geometry. * Item geometry is replaced by pipe output unless an alternative property name is given in the constructor. */ public class Union extends AbstractGeoPipe { - + private Geometry other = null; - + public Union() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public Union(String resultPropertyName) { super(resultPropertyName); - } + } public Union(Geometry other) { this.other = other; - } - + } + public Union(Geometry other, String resultPropertyName) { super(resultPropertyName); this.other = other; - } - - @Override + } + + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { if (other == null) { setGeometry(flow, flow.getGeometry().union()); } else { - setGeometry(flow, flow.getGeometry().union(other)); + setGeometry(flow, flow.getGeometry().union(other)); } return flow; } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/UnionAll.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/UnionAll.java index debabc1e5..c05b237f3 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/UnionAll.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/UnionAll.java @@ -29,7 +29,7 @@ */ public class UnionAll extends AbstractGroupGeoPipe { - @Override + @Override protected void group(GeoPipeFlow flow) { if (groups.size() == 0) { groups.add(flow); @@ -40,4 +40,4 @@ protected void group(GeoPipeFlow flow) { } } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/pipes/processing/WellKnownText.java b/src/main/java/org/neo4j/gis/spatial/pipes/processing/WellKnownText.java index 9dda4934c..e40ec4337 100644 --- a/src/main/java/org/neo4j/gis/spatial/pipes/processing/WellKnownText.java +++ b/src/main/java/org/neo4j/gis/spatial/pipes/processing/WellKnownText.java @@ -19,31 +19,30 @@ */ package org.neo4j.gis.spatial.pipes.processing; +import org.locationtech.jts.io.WKTWriter; import org.neo4j.gis.spatial.pipes.AbstractGeoPipe; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; -import org.locationtech.jts.io.WKTWriter; - /** * Encodes item geometry to Well Known Text (WKT). */ public class WellKnownText extends AbstractGeoPipe { public WellKnownText() { - } - + } + /** * @param resultPropertyName property name to use for geometry output - */ + */ public WellKnownText(String resultPropertyName) { super(resultPropertyName); - } + } - @Override + @Override protected GeoPipeFlow process(GeoPipeFlow flow) { WKTWriter wktWriter = new WKTWriter(); setProperty(flow, wktWriter.write(flow.getGeometry())); return flow; - } - -} \ No newline at end of file + } + +} diff --git a/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java b/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java index 838248e8f..81af7ce28 100644 --- a/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java +++ b/src/main/java/org/neo4j/gis/spatial/procedures/SpatialProcedures.java @@ -19,12 +19,42 @@ */ package org.neo4j.gis.spatial.procedures; -import javax.annotation.Nullable; +import static org.neo4j.gis.spatial.SpatialDatabaseService.RTREE_INDEX_NAME; +import static org.neo4j.gis.spatial.encoders.neo4j.Neo4jCRS.findCRS; +import static org.neo4j.internal.helpers.collection.MapUtil.map; +import static org.neo4j.procedure.Mode.WRITE; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.geotools.api.referencing.ReferenceIdentifier; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.referencing.crs.DefaultGeographicCRS; -import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.EditableLayer; +import org.neo4j.gis.spatial.EditableLayerImpl; +import org.neo4j.gis.spatial.GeometryEncoder; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.ShapefileImporter; +import org.neo4j.gis.spatial.SimplePointLayer; +import org.neo4j.gis.spatial.SpatialDatabaseService; +import org.neo4j.gis.spatial.SpatialTopologyUtils; +import org.neo4j.gis.spatial.WKBGeometryEncoder; +import org.neo4j.gis.spatial.WKTGeometryEncoder; import org.neo4j.gis.spatial.encoders.NativePointEncoder; import org.neo4j.gis.spatial.encoders.SimpleGraphEncoder; import org.neo4j.gis.spatial.encoders.SimplePointEncoder; @@ -55,22 +85,11 @@ import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.logging.Level; import org.neo4j.logging.Log; -import org.neo4j.procedure.*; -import org.geotools.api.referencing.ReferenceIdentifier; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.*; -import java.util.function.BiFunction; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.neo4j.gis.spatial.SpatialDatabaseService.RTREE_INDEX_NAME; -import static org.neo4j.gis.spatial.encoders.neo4j.Neo4jCRS.findCRS; -import static org.neo4j.internal.helpers.collection.MapUtil.map; -import static org.neo4j.procedure.Mode.WRITE; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.Procedure; +import org.neo4j.procedure.UserFunction; /* TODO: @@ -80,980 +99,1039 @@ public class SpatialProcedures { - @Context - public GraphDatabaseService db; - - @Context - public GraphDatabaseAPI api; - - @Context - public Transaction tx; - - @Context - public KernelTransaction ktx; - - @Context - public Log log; - - private SpatialDatabaseService spatial() { - return new SpatialDatabaseService(new IndexManager(api, ktx.securityContext())); - } - - public static class NodeResult { - public final Node node; - - public NodeResult(Node node) { - this.node = node; - } - } - - public static class NodeIdResult { - public final String nodeId; - - public NodeIdResult(String nodeId) { - this.nodeId = nodeId; - } - } - - public static class CountResult { - public final long count; - - public CountResult(long count) { - this.count = count; - } - } - - public static class NameResult { - public String name; - public final String signature; - - public NameResult(String name, String signature) { - this.name = name; - this.signature = signature; - } - } - - public static class StringResult { - public final String name; - - public StringResult(String name) { - this.name = name; - } - } - - public static class NodeDistanceResult { - public final Node node; - public final double distance; - - public NodeDistanceResult(Node node, double distance) { - this.node = node; - this.distance = distance; - } - } - - public static class GeometryResult { - public final Object geometry; - - public GeometryResult(org.neo4j.graphdb.spatial.Geometry geometry) { - // Unfortunately Neo4j 3.4 only copes with Points, other types need to be converted to a public type - if(geometry instanceof org.neo4j.graphdb.spatial.Point) { - this.geometry = geometry; - }else{ - this.geometry = toMap(geometry); - } - } - } - - private static Map encoderClasses = new HashMap<>(); - - static { - populateEncoderClasses(); - } - - private static void populateEncoderClasses() { - encoderClasses.clear(); - // TODO: Make this auto-find classes that implement GeometryEncoder - for (Class cls : new Class[]{ - SimplePointEncoder.class, OSMGeometryEncoder.class, SimplePropertyEncoder.class, - WKTGeometryEncoder.class, WKBGeometryEncoder.class, SimpleGraphEncoder.class, - NativePointEncoder.class - }) { - if (GeometryEncoder.class.isAssignableFrom(cls)) { - String name = cls.getSimpleName(); - encoderClasses.put(name, cls); - } - } - } - - @Procedure("spatial.procedures") - @Description("Lists all spatial procedures with name and signature") - public Stream listProcedures() throws ProcedureException { - GlobalProcedures procedures = ((GraphDatabaseAPI) db).getDependencyResolver().resolveDependency(GlobalProcedures.class); - Stream.Builder builder = Stream.builder(); - for (ProcedureSignature proc : procedures.getCurrentView().getAllProcedures()) { - if (proc.name().namespace()[0].equals("spatial")) { - builder.accept(new NameResult(proc.name().toString(), proc.toString())); - } - } - return builder.build(); - } - - @Procedure(name = "spatial.upgrade", mode = WRITE) - @Description("Upgrades an older spatial data model and returns a list of layers upgraded") - public Stream upgradeSpatial() { - SpatialDatabaseService sdb = spatial(); - Stream.Builder builder = Stream.builder(); - for (String name : sdb.upgradeFromOldModel(tx)) { - Layer layer = sdb.getLayer(tx, name); - if (layer != null) { - builder.accept(new NameResult(name, layer.getSignature())); - } - } - return builder.build(); - } - - @Procedure(value="spatial.layers", mode = WRITE) - @Description("Returns name, and details for all layers") - public Stream getAllLayers() { - SpatialDatabaseService sdb = spatial(); - Stream.Builder builder = Stream.builder(); - for (String name : sdb.getLayerNames(tx)) { - Layer layer = sdb.getLayer(tx, name); - if (layer != null) { - builder.accept(new NameResult(name, layer.getSignature())); - } - } - return builder.build(); - } - - @Procedure("spatial.layerTypes") - @Description("Returns the different registered layer types") - public Stream getAllLayerTypes() { - SpatialDatabaseService sdb = spatial(); - Stream.Builder builder = Stream.builder(); - for (Map.Entry entry : sdb.getRegisteredLayerTypes().entrySet()) { - builder.accept(new NameResult(entry.getKey(), entry.getValue())); - } - return builder.build(); - } - - @Procedure(value="spatial.addPointLayer", mode=WRITE) - @Description("Adds a new simple point layer, returns the layer root node") - public Stream addSimplePointLayer( - @Name("name") String name, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, - sdb.resolveIndexClass(indexType), null, - selectCRS(crsName)).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addPointLayerGeohash", mode=WRITE) - @Description("Adds a new simple point layer with geohash based index, returns the layer root node") - public Stream addSimplePointLayerGeohash( - @Name("name") String name, - @Name(value = "crsName", defaultValue = WGS84_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, - LayerGeohashPointIndex.class, null, - selectCRS(crsName)).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addPointLayerZOrder", mode=WRITE) - @Description("Adds a new simple point layer with z-order curve based index, returns the layer root node") - public Stream addSimplePointLayerZOrder(@Name("name") String name) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, LayerZOrderPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addPointLayerHilbert", mode=WRITE) - @Description("Adds a new simple point layer with hilbert curve based index, returns the layer root node") - public Stream addSimplePointLayerHilbert(@Name("name") String name) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, LayerHilbertPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addPointLayerXY", mode=WRITE) - @Description("Adds a new simple point layer with the given properties for x and y coordinates, returns the layer root node") - public Stream addSimplePointLayer( - @Name("name") String name, - @Name("xProperty") String xProperty, - @Name("yProperty") String yProperty, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - if (xProperty != null && yProperty != null) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, - sdb.resolveIndexClass(indexType), sdb.makeEncoderConfig(xProperty, yProperty), - selectCRS(hintCRSName(crsName, yProperty))).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': Missing encoder config values: xProperty[" + xProperty + "], yProperty[" + yProperty + "]"); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addPointLayerWithConfig", mode=WRITE) - @Description("Adds a new simple point layer with the given configuration, returns the layer root node") - public Stream addSimplePointLayerWithConfig( - @Name("name") String name, - @Name("encoderConfig") String encoderConfig, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - if (encoderConfig.indexOf(':') > 0) { - return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, - sdb.resolveIndexClass(indexType), encoderConfig, - selectCRS(hintCRSName(crsName, encoderConfig))).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': invalid encoder config '" + encoderConfig + "'"); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayer", mode=WRITE) - @Description("Adds a new native point layer, returns the layer root node") - public Stream addNativePointLayer( - @Name("name") String name, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, sdb.resolveIndexClass(indexType), null, selectCRS(crsName)).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayerGeohash", mode=WRITE) - @Description("Adds a new native point layer with geohash based index, returns the layer root node") - public Stream addNativePointLayerGeohash( - @Name("name") String name, - @Name(value = "crsName", defaultValue = WGS84_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, LayerGeohashPointIndex.class, null, selectCRS(crsName)).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayerZOrder", mode=WRITE) - @Description("Adds a new native point layer with z-order curve based index, returns the layer root node") - public Stream addNativePointLayerZOrder(@Name("name") String name) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, LayerZOrderPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayerHilbert", mode=WRITE) - @Description("Adds a new native point layer with hilbert curve based index, returns the layer root node") - public Stream addNativePointLayerHilbert(@Name("name") String name) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, LayerHilbertPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayerXY", mode=WRITE) - @Description("Adds a new native point layer with the given properties for x and y coordinates, returns the layer root node") - public Stream addNativePointLayer( - @Name("name") String name, - @Name("xProperty") String xProperty, - @Name("yProperty") String yProperty, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - if (xProperty != null && yProperty != null) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, - sdb.resolveIndexClass(indexType), sdb.makeEncoderConfig(xProperty, yProperty), - selectCRS(hintCRSName(crsName, yProperty))).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': Missing encoder config values: xProperty[" + xProperty + "], yProperty[" + yProperty + "]"); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addNativePointLayerWithConfig", mode=WRITE) - @Description("Adds a new native point layer with the given configuration, returns the layer root node") - public Stream addNativePointLayerWithConfig( - @Name("name") String name, - @Name("encoderConfig") String encoderConfig, - @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, - @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - if (encoderConfig.indexOf(':') > 0) { - return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, - sdb.resolveIndexClass(indexType), encoderConfig, - selectCRS(hintCRSName(crsName, encoderConfig))).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': invalid encoder config '" + encoderConfig + "'"); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - public static final String UNSET_CRS_NAME = ""; - public static final String WGS84_CRS_NAME = "wgs84"; - - /** - * Currently this only supports the string 'WGS84', for the convenience of procedure users. - * This should be expanded with CRS table lookup. - * @param name - * @return null or WGS84 - */ - public CoordinateReferenceSystem selectCRS(String name) { - if (name == null) { - return null; - } else { - switch (name.toLowerCase()) { - case WGS84_CRS_NAME: - return org.geotools.referencing.crs.DefaultGeographicCRS.WGS84; - case UNSET_CRS_NAME: - return null; - default: - throw new IllegalArgumentException("Unsupported CRS name: " + name); - } - } - } - - private String hintCRSName(String crsName, String hint) { - if (crsName.equals(UNSET_CRS_NAME) && hint.toLowerCase().contains("lat")) { - crsName = WGS84_CRS_NAME; - } - return crsName; - } - - @Procedure(value="spatial.addLayerWithEncoder", mode=WRITE) - @Description("Adds a new layer with the given encoder class and configuration, returns the layer root node") - public Stream addLayerWithEncoder( - @Name("name") String name, - @Name("encoder") String encoderClassName, - @Name("encoderConfig") String encoderConfig) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - Class encoderClass = encoderClasses.get(encoderClassName); - Class layerClass = sdb.suggestLayerClassForEncoder(encoderClass); - if (encoderClass != null) { - return streamNode(sdb.createLayer(tx, name, encoderClass, layerClass, null, encoderConfig).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': invalid encoder class '" + encoderClassName + "'"); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - @Procedure(value="spatial.addLayer", mode=WRITE) - @Description("Adds a new layer with the given type (see spatial().getAllLayerTypes) and configuration, returns the layer root node") - public Stream addLayerOfType( - @Name("name") String name, - @Name("type") String type, - @Name("encoderConfig") String encoderConfig) { - SpatialDatabaseService sdb = spatial(); - Layer layer = sdb.getLayer(tx, name); - if (layer == null) { - Map knownTypes = sdb.getRegisteredLayerTypes(); - if (knownTypes.containsKey(type.toLowerCase())) { - return streamNode(sdb.getOrCreateRegisteredTypeLayer(tx, name, type, encoderConfig).getLayerNode(tx)); - } else { - throw new IllegalArgumentException("Cannot create layer '" + name + "': unknown type '" + type + "' - supported types are " + knownTypes.toString()); - } - } else { - throw new IllegalArgumentException("Cannot create existing layer: " + name); - } - } - - private Stream streamNode(Node node) { - return Stream.of(new NodeResult(node)); - } - - private Stream streamNode(String nodeId) { - return Stream.of(new NodeIdResult(nodeId)); - } - - @Procedure(value="spatial.addWKTLayer", mode=WRITE) - @Description("Adds a new WKT layer with the given node property to hold the WKT string, returns the layer root node") - public Stream addWKTLayer(@Name("name") String name, - @Name("nodePropertyName") String nodePropertyName) { - return addLayerOfType(name, "WKT", nodePropertyName); - } - - @Procedure(value="spatial.layer", mode=WRITE) - @Description("Returns the layer root node for the given layer name") - public Stream getLayer(@Name("name") String name) { - return streamNode(getLayerOrThrow(tx, spatial(), name).getLayerNode(tx)); - } - - @Procedure(value="spatial.getFeatureAttributes", mode=WRITE) - @Description("Returns feature attributes of the given layer") - public Stream getFeatureAttributes(@Name("name") String name) { - Layer layer = getLayerOrThrow(tx, spatial(), name); - return Arrays.asList(layer.getExtraPropertyNames(tx)).stream().map(StringResult::new); - } - - @Procedure(value="spatial.setFeatureAttributes", mode=WRITE) - @Description("Sets the feature attributes of the given layer") - public Stream setFeatureAttributes(@Name("name") String name, - @Name("attributeNames") List attributeNames) { - EditableLayerImpl layer = getEditableLayerOrThrow(tx, spatial(), name); - layer.setExtraPropertyNames(attributeNames.toArray(new String[attributeNames.size()]), tx); - return streamNode(layer.getLayerNode(tx)); - } - - @Procedure(value="spatial.removeLayer", mode=WRITE) - @Description("Removes the given layer") - public void removeLayer(@Name("name") String name) { - SpatialDatabaseService sdb = spatial(); - sdb.deleteLayer(tx, name, new ProgressLoggingListener("Deleting layer '" + name + "'", log, Level.INFO)); - } - - @Procedure(value="spatial.addNode", mode=WRITE) - @Description("Adds the given node to the layer, returns the geometry-node") - public Stream addNodeToLayer(@Name("layerName") String name, @Name("node") Node node) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - return streamNode(layer.add(tx, node).getGeomNode()); - } - - @Procedure(value="spatial.addNodes", mode=WRITE) - @Description("Adds the given nodes list to the layer, returns the count") - public Stream addNodesToLayer(@Name("layerName") String name, @Name("nodes") List nodes) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - return Stream.of(new CountResult(layer.addAll(tx, nodes))); - } - - @Procedure(value="spatial.addNode.byId", mode=WRITE) - @Description("Adds the given node to the layer, returns the geometry-node") - public Stream addNodeIdToLayer(@Name("layerName") String name, @Name("nodeId") String nodeId) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - return streamNode(layer.add(tx, tx.getNodeByElementId(nodeId)).getGeomNode()); - } - - @Procedure(value="spatial.addNodes.byId", mode=WRITE) - @Description("Adds the given nodes list to the layer, returns the count") - public Stream addNodeIdsToLayer(@Name("layerName") String name, @Name("nodeIds") List nodeIds) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - List nodes = nodeIds.stream().map(id -> tx.getNodeByElementId(id)).collect(Collectors.toList()); - return Stream.of(new CountResult(layer.addAll(tx, nodes))); - } - - @Procedure(value="spatial.removeNode", mode=WRITE) - @Description("Removes the given node from the layer, returns the geometry-node") - public Stream removeNodeFromLayer(@Name("layerName") String name, @Name("node") Node node) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - layer.removeFromIndex(tx, node.getElementId()); - return streamNode(node.getElementId()); - } - - @Procedure(value="spatial.removeNodes", mode=WRITE) - @Description("Removes the given nodes from the layer, returns the count of nodes removed") - public Stream removeNodesFromLayer(@Name("layerName") String name, @Name("nodes") List nodes) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - //TODO optimize bulk node removal from RTree like we have done for node additions - int before = layer.getIndex().count(tx); - for (Node node : nodes) { - layer.removeFromIndex(tx, node.getElementId()); - } - int after = layer.getIndex().count(tx); - return Stream.of(new CountResult(before - after)); - } - - @Procedure(value="spatial.removeNode.byId", mode=WRITE) - @Description("Removes the given node from the layer, returns the geometry-node") - public Stream removeNodeFromLayer(@Name("layerName") String name, @Name("nodeId") String nodeId) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - layer.removeFromIndex(tx, nodeId); - return streamNode(nodeId); - } - - @Procedure(value="spatial.removeNodes.byId", mode=WRITE) - @Description("Removes the given nodes from the layer, returns the count of nodes removed") - public Stream removeNodeIdsFromLayer(@Name("layerName") String name, @Name("nodeIds") List nodeIds) { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - //TODO optimize bulk node removal from RTree like we have done for node additions - int before = layer.getIndex().count(tx); - for (String nodeId : nodeIds) { - layer.removeFromIndex(tx, nodeId); - } - int after = layer.getIndex().count(tx); - return Stream.of(new CountResult(before - after)); - } - - @Procedure(value="spatial.addWKT", mode=WRITE) - @Description("Adds the given WKT string to the layer, returns the created geometry node") - public Stream addGeometryWKTToLayer(@Name("layerName") String name, @Name("geometry") String geometryWKT) throws ParseException { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - WKTReader reader = new WKTReader(layer.getGeometryFactory()); - return streamNode(addGeometryWkt(layer, reader, geometryWKT)); - } - - @Procedure(value="spatial.addWKTs", mode=WRITE) - @Description("Adds the given WKT string list to the layer, returns the created geometry nodes") - public Stream addGeometryWKTsToLayer(@Name("layerName") String name, @Name("geometry") List geometryWKTs) throws ParseException { - EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); - WKTReader reader = new WKTReader(layer.getGeometryFactory()); - return geometryWKTs.stream().map(geometryWKT -> addGeometryWkt(layer, reader, geometryWKT)).map(NodeResult::new); - } - - private Node addGeometryWkt(EditableLayer layer, WKTReader reader, String geometryWKT) { - try { - Geometry geometry = reader.read(geometryWKT); - return layer.add(tx, geometry).getGeomNode(); - } catch (ParseException e) { - throw new RuntimeException("Error parsing geometry: " + geometryWKT, e); - } - } - - @Procedure(value="spatial.importShapefileToLayer", mode=WRITE) - @Description("Imports the the provided shape-file from URI to the given layer, returns the count of data added") - public Stream importShapefile( - @Name("layerName") String name, - @Name("uri") String uri) throws IOException { - EditableLayerImpl layer = getEditableLayerOrThrow(tx, spatial(), name); - return Stream.of(new CountResult(importShapefileToLayer(uri, layer, 1000).size())); - } - - @Procedure(value="spatial.importShapefile", mode=WRITE) - @Description("Imports the the provided shape-file from URI to a layer of the same name, returns the count of data added") - public Stream importShapefile( - @Name("uri") String uri) throws IOException { - return Stream.of(new CountResult(importShapefileToLayer(uri, null, 1000).size())); - } - - private List importShapefileToLayer(String shpPath, EditableLayerImpl layer, int commitInterval) throws IOException { - if (shpPath.toLowerCase().endsWith(".shp")) { - // remove extension - shpPath = shpPath.substring(0, shpPath.lastIndexOf(".")); - } - - ShapefileImporter importer = new ShapefileImporter(db, new ProgressLoggingListener("Importing " + shpPath, log, Level.DEBUG), commitInterval); - if (layer == null) { - String layerName = shpPath.substring(shpPath.lastIndexOf(File.separator) + 1); - return importer.importFile(shpPath, layerName); - } else { - return importer.importFile(shpPath, layer, Charset.defaultCharset()); - } - } - - @Procedure(value="spatial.importOSMToLayer", mode=WRITE) - @Description("Imports the the provided osm-file from URI to a layer, returns the count of data added") - public Stream importOSM( - @Name("layerName") String layerName, - @Name("uri") String uri) throws InterruptedException { - // Delegate finding the layer to the inner thread, so we do not pollute the procedure transaction with anything that might conflict. - // Since the procedure transaction starts before, and ends after, all inner transactions. - BiFunction layerFinder = (tx, name) -> (OSMLayer) getEditableLayerOrThrow(tx, spatial(), name); - return Stream.of(new CountResult(importOSMToLayer(uri, layerName, layerFinder))); - } - - @Procedure(value="spatial.importOSM", mode=WRITE) - @Description("Imports the the provided osm-file from URI to a layer of the same name, returns the count of data added") - public Stream importOSM( - @Name("uri") String uri) throws InterruptedException { - String layerName = uri.substring(uri.lastIndexOf(File.separator) + 1); - assertLayerDoesNotExists(tx, spatial(), layerName); - // Delegate creating the layer to the inner thread, so we do not pollute the procedure transaction with anything that might conflict. - // Since the procedure transaction starts before, and ends after, all inner transactions. - BiFunction layerMaker = (tx, name) -> (OSMLayer) spatial().getOrCreateLayer(tx, name, OSMGeometryEncoder.class, OSMLayer.class); - return Stream.of(new CountResult(importOSMToLayer(uri, layerName, layerMaker))); - } - - private long importOSMToLayer(String osmPath, String layerName, BiFunction layerMaker) throws InterruptedException { - if (!osmPath.toLowerCase().endsWith(".osm")) { - // add extension - osmPath = osmPath + ".osm"; - } - OSMImportRunner runner = new OSMImportRunner(api, ktx.securityContext(), osmPath, layerName, layerMaker, log, Level.DEBUG); - Thread importerThread = new Thread(runner); - importerThread.start(); - importerThread.join(); - return runner.getResult(); - } - - private static class OSMImportRunner implements Runnable { - private final GraphDatabaseAPI db; - private final String osmPath; - private final String layerName; - private final BiFunction layerMaker; - private final Log log; - private final Level level; - private final SecurityContext securityContext; - private Exception e; - private long rc = -1; - - OSMImportRunner(GraphDatabaseAPI db, SecurityContext securityContext, String osmPath, String layerName, BiFunction layerMaker, Log log, Level level) { - this.db = db; - this.osmPath = osmPath; - this.layerName = layerName; - this.layerMaker = layerMaker; - this.log = log; - this.level = level; - this.securityContext = securityContext; - } - - long getResult() { - if (e == null) { - return rc; - } else { - throw new RuntimeException("Failed to import " + osmPath + " to layer '" + layerName + "': " + e.getMessage(), e); - } - } - - @Override - public void run() { - // Create the layer in the same thread as doing the import, otherwise we have an outer thread doing a create, - // and the inner thread repeating it, resulting in duplicates - try (Transaction tx = db.beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext)) { - layerMaker.apply(tx, layerName); - tx.commit(); - } - OSMImporter importer = new OSMImporter(layerName, new ProgressLoggingListener("Importing " + osmPath, log, level)); - try { - // Provide the security context for all inner transactions that will be made during import - importer.setSecurityContext(securityContext); - // import using multiple, serial inner transactions (using the security context of the outer thread) - importer.importFile(db, osmPath, false, 10000); - // Re-index using inner transactions (using the security context of the outer thread) - rc = importer.reIndex(db, 10000, false); - } catch (Exception e) { - log.error("Error running OSMImporter: " + e.getMessage()); - this.e = e; - } - } - } - - @Procedure(value="spatial.bbox", mode=WRITE) - @Description("Finds all geometry nodes in the given layer within the lower left and upper right coordinates of a box") - public Stream findGeometriesInBBox( - @Name("layerName") String name, - @Name("min") Object min, - @Name("max") Object max) { - Layer layer = getLayerOrThrow(tx, spatial(), name); - // TODO why a SearchWithin and not a SearchIntersectWindow? - Envelope envelope = new Envelope(toCoordinate(min), toCoordinate(max)); - return GeoPipeline - .startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(envelope)) - .stream().map(GeoPipeFlow::getGeomNode).map(NodeResult::new); - } - - @Procedure(value="spatial.closest", mode=WRITE) - @Description("Finds all geometry nodes in the layer within the distance to the given coordinate") - public Stream findClosestGeometries( - @Name("layerName") String name, - @Name("coordinate") Object coordinate, - @Name("distanceInKm") double distanceInKm) { - Layer layer = getLayerOrThrow(tx, spatial(), name); - GeometryFactory factory = layer.getGeometryFactory(); - Point point = factory.createPoint(toCoordinate(coordinate)); - List edgeResults = SpatialTopologyUtils.findClosestEdges(tx, point, layer, distanceInKm); - return edgeResults.stream().map(e -> e.getValue().getGeomNode()).map(NodeResult::new); - } - - @Procedure(value="spatial.withinDistance", mode=WRITE) - @Description("Returns all geometry nodes and their ordered distance in the layer within the distance to the given coordinate") - public Stream findGeometriesWithinDistance( - @Name("layerName") String name, - @Name("coordinate") Object coordinate, - @Name("distanceInKm") double distanceInKm) { - - Layer layer = getLayerOrThrow(tx, spatial(), name); - return GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, toCoordinate(coordinate), distanceInKm) - .sort(OrthodromicDistance.DISTANCE) - .stream().map(r -> { - double distance = r.hasProperty(tx, OrthodromicDistance.DISTANCE) ? ((Number) r.getProperty(tx, OrthodromicDistance.DISTANCE)).doubleValue() : -1; - return new NodeDistanceResult(r.getGeomNode(), distance); - }); - } - - @UserFunction("spatial.decodeGeometry") - @Description("Returns a geometry of a layer node as the Neo4j geometry type, to be passed to other procedures or returned to a client") - public Object decodeGeometry( - @Name("layerName") String name, - @Name("node") Node node) { - - Layer layer = getLayerOrThrow(tx, spatial(), name); - GeometryResult result = new GeometryResult(toNeo4jGeometry(layer, layer.getGeometryEncoder().decodeGeometry(node))); - return result.geometry; - } - - @UserFunction("spatial.asMap") - @Description("Returns a Map object representing the Geometry, to be passed to other procedures or returned to a client") - public Object asMap(@Name("object") Object geometry) { - return toGeometryMap(geometry); - } - - @UserFunction("spatial.asGeometry") - @Description("Returns a geometry object as the Neo4j geometry type, to be passed to other functions or procedures or returned to a client") - public Object asGeometry( - @Name("geometry") Object geometry) { - - return toNeo4jGeometry(null, geometry); - } - - @Deprecated - @Procedure("spatial.asGeometry") - @Description("Returns a geometry object as the Neo4j geometry type, to be passed to other procedures or returned to a client") - public Stream asGeometryProc( - @Name("geometry") Object geometry) { - - return Stream.of(geometry).map(geom -> new GeometryResult(toNeo4jGeometry(null, geom))); - } - - @Deprecated - @Procedure(value = "spatial.asExternalGeometry", deprecatedBy = "spatial.asGeometry") - @Description("Returns a geometry object as an external geometry type to be returned to a client") - // This only existed temporarily because the other method, asGeometry, returned the wrong type due to a bug in Neo4j 3.0 - public Stream asExternalGeometry( - @Name("geometry") Object geometry) { - - return Stream.of(geometry).map(geom -> new GeometryResult(toNeo4jGeometry(null, geom))); - } - - @Procedure(value="spatial.intersects", mode=WRITE) - @Description("Returns all geometry nodes that intersect the given geometry (shape, polygon) in the layer") - public Stream findGeometriesIntersecting( - @Name("layerName") String name, - @Name("geometry") Object geometry) { - - Layer layer = getLayerOrThrow(tx, spatial(), name); - return GeoPipeline - .startIntersectSearch(tx, layer, toJTSGeometry(layer, geometry)) - .stream().map(GeoPipeFlow::getGeomNode).map(NodeResult::new); - } - - private Geometry toJTSGeometry(Layer layer, Object value) { - GeometryFactory factory = layer.getGeometryFactory(); - if (value instanceof org.neo4j.graphdb.spatial.Point) { - org.neo4j.graphdb.spatial.Point point = (org.neo4j.graphdb.spatial.Point) value; - double[] coord = point.getCoordinate().getCoordinate(); - return factory.createPoint(new Coordinate(coord[0], coord[1])); - } - if (value instanceof String) { - WKTReader reader = new WKTReader(factory); - try { - return reader.read((String) value); - } catch (ParseException e) { - throw new IllegalArgumentException("Invalid WKT: " + e.getMessage()); - } - } - Map latLon = null; - if (value instanceof Entity) { - latLon = ((Entity) value).getProperties("latitude", "longitude", "lat", "lon"); - } - if (value instanceof Map) latLon = (Map) value; - Coordinate coord = toCoordinate(latLon); - if (coord != null) return factory.createPoint(coord); - throw new RuntimeException("Can't convert " + value + " to a geometry"); - } - - private static org.neo4j.graphdb.spatial.Coordinate toNeo4jCoordinate(Coordinate coordinate) { - if (coordinate.z == Coordinate.NULL_ORDINATE) { - return new org.neo4j.graphdb.spatial.Coordinate(coordinate.x, coordinate.y); - } else { - return new org.neo4j.graphdb.spatial.Coordinate(coordinate.x, coordinate.y, coordinate.z); - } - } - - private static List toNeo4jCoordinates(Coordinate[] coordinates) { - ArrayList converted = new ArrayList<>(); - for (Coordinate coordinate : coordinates) { - converted.add(toNeo4jCoordinate(coordinate)); - } - return converted; - } - - private org.neo4j.graphdb.spatial.Geometry toNeo4jGeometry(Layer layer, Object value) { - if (value instanceof org.neo4j.graphdb.spatial.Geometry) { - return (org.neo4j.graphdb.spatial.Geometry) value; - } - Neo4jCRS crs = findCRS("Cartesian"); - if (layer != null) { - CoordinateReferenceSystem layerCRS = layer.getCoordinateReferenceSystem(tx); - if (layerCRS != null) { - ReferenceIdentifier crsRef = layer.getCoordinateReferenceSystem(tx).getName(); - crs = findCRS(crsRef.toString()); - } - } - if (value instanceof Point) { - Point point = (Point) value; - return new Neo4jPoint(point, crs); - } - if (value instanceof Geometry) { - Geometry geometry = (Geometry) value; - return new Neo4jGeometry(geometry.getGeometryType(), toNeo4jCoordinates(geometry.getCoordinates()), crs); - } - if (value instanceof String && layer != null) { - GeometryFactory factory = layer.getGeometryFactory(); - WKTReader reader = new WKTReader(factory); - try { - Geometry geometry = reader.read((String) value); - return new Neo4jGeometry(geometry.getGeometryType(), toNeo4jCoordinates(geometry.getCoordinates()), crs); - } catch (ParseException e) { - throw new IllegalArgumentException("Invalid WKT: " + e.getMessage()); - } - } - Map latLon = null; - if (value instanceof Entity) { - latLon = ((Entity) value).getProperties("latitude", "longitude", "lat", "lon"); - } - if (value instanceof Map) latLon = (Map) value; - Coordinate coord = toCoordinate(latLon); - if (coord != null) return new Neo4jPoint(coord, crs); - throw new RuntimeException("Can't convert " + value + " to a geometry"); - } - - private Object toPublic(Object obj) { - if (obj instanceof Map) { - return toPublic((Map) obj); - } else if (obj instanceof Entity) { - return toPublic(((Entity) obj).getProperties()); - } else if (obj instanceof Geometry) { - return toMap((Geometry) obj); - } else { - return obj; - } - } - - private Map toGeometryMap(Object geometry) { - return toMap(toNeo4jGeometry(null, geometry)); - } - - private Map toMap(Geometry geometry) { - return toMap(toNeo4jGeometry(null, geometry)); - } - - private static double[][] toCoordinateArrayFromCoordinates(List coords) { - List coordinates = new ArrayList<>(coords.size()); - for (org.neo4j.graphdb.spatial.Coordinate coord : coords) { - coordinates.add(coord.getCoordinate()); - } - return toCoordinateArray(coordinates); - } - - private static double[][] toCoordinateArray(List coords) { - double[][] coordinates = new double[coords.size()][]; - for (int i = 0; i < coordinates.length; i++) { - coordinates[i] = coords.get(i); - } - return coordinates; - } - - private static Map toMap(org.neo4j.graphdb.spatial.Geometry geometry) { - if (geometry instanceof org.neo4j.graphdb.spatial.Point) { - org.neo4j.graphdb.spatial.Point point = (org.neo4j.graphdb.spatial.Point) geometry; - return map("type", geometry.getGeometryType(), "coordinate", point.getCoordinate().getCoordinate()); - } else { - return map("type", geometry.getGeometryType(), "coordinates", toCoordinateArrayFromCoordinates(geometry.getCoordinates())); - } - } - - private Map toPublic(Map incoming) { - Map map = new HashMap<>(incoming.size()); - for (Object key : incoming.keySet()) { - map.put(key.toString(), toPublic(incoming.get(key))); - } - return map; - } - - private Coordinate toCoordinate(Object value) { - if (value instanceof Coordinate) { - return (Coordinate) value; - } - if (value instanceof org.neo4j.graphdb.spatial.Coordinate) { - return toCoordinate((org.neo4j.graphdb.spatial.Coordinate) value); - } - if (value instanceof org.neo4j.graphdb.spatial.Point) { - return toCoordinate(((org.neo4j.graphdb.spatial.Point) value).getCoordinate()); - } - if (value instanceof Entity) { - return toCoordinate(((Entity) value).getProperties("latitude", "longitude", "lat", "lon")); - } - if (value instanceof Map) { - return toCoordinate((Map) value); - } - throw new RuntimeException("Can't convert " + value + " to a coordinate"); - } - - private static Coordinate toCoordinate(org.neo4j.graphdb.spatial.Coordinate point) { - double[] coordinate = point.getCoordinate(); - return new Coordinate(coordinate[0], coordinate[1]); - } - - private static Coordinate toCoordinate(Map map) { - if (map == null) return null; - Coordinate coord = toCoordinate(map, "longitude", "latitude"); - if (coord == null) return toCoordinate(map, "lon", "lat"); - return coord; - } - - private static Coordinate toCoordinate(Map map, String xName, String yName) { - if (map.containsKey(xName) && map.containsKey(yName)) - return new Coordinate(((Number) map.get(xName)).doubleValue(), ((Number) map.get(yName)).doubleValue()); - return null; - } - - private static EditableLayerImpl getEditableLayerOrThrow(Transaction tx, SpatialDatabaseService spatial, String name) { - return (EditableLayerImpl) getLayerOrThrow(tx, spatial, name); - } - - private static Layer getLayerOrThrow(Transaction tx, SpatialDatabaseService spatial, String name) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, name); - if (layer != null) { - return layer; - } else { - throw new IllegalArgumentException("No such layer '" + name + "'"); - } - } - - private static void assertLayerDoesNotExists(Transaction tx, SpatialDatabaseService spatial, String name) { - if (spatial.getLayer(tx, name) != null) { - throw new IllegalArgumentException("Layer already exists: '" + name + "'"); - } - } + @Context + public GraphDatabaseService db; + + @Context + public GraphDatabaseAPI api; + + @Context + public Transaction tx; + + @Context + public KernelTransaction ktx; + + @Context + public Log log; + + private SpatialDatabaseService spatial() { + return new SpatialDatabaseService(new IndexManager(api, ktx.securityContext())); + } + + public static class NodeResult { + + public final Node node; + + public NodeResult(Node node) { + this.node = node; + } + } + + public static class NodeIdResult { + + public final String nodeId; + + public NodeIdResult(String nodeId) { + this.nodeId = nodeId; + } + } + + public static class CountResult { + + public final long count; + + public CountResult(long count) { + this.count = count; + } + } + + public static class NameResult { + + public String name; + public final String signature; + + public NameResult(String name, String signature) { + this.name = name; + this.signature = signature; + } + } + + public static class StringResult { + + public final String name; + + public StringResult(String name) { + this.name = name; + } + } + + public static class NodeDistanceResult { + + public final Node node; + public final double distance; + + public NodeDistanceResult(Node node, double distance) { + this.node = node; + this.distance = distance; + } + } + + public static class GeometryResult { + + public final Object geometry; + + public GeometryResult(org.neo4j.graphdb.spatial.Geometry geometry) { + // Unfortunately Neo4j 3.4 only copes with Points, other types need to be converted to a public type + if (geometry instanceof org.neo4j.graphdb.spatial.Point) { + this.geometry = geometry; + } else { + this.geometry = toMap(geometry); + } + } + } + + private static Map encoderClasses = new HashMap<>(); + + static { + populateEncoderClasses(); + } + + private static void populateEncoderClasses() { + encoderClasses.clear(); + // TODO: Make this auto-find classes that implement GeometryEncoder + for (Class cls : new Class[]{ + SimplePointEncoder.class, OSMGeometryEncoder.class, SimplePropertyEncoder.class, + WKTGeometryEncoder.class, WKBGeometryEncoder.class, SimpleGraphEncoder.class, + NativePointEncoder.class + }) { + if (GeometryEncoder.class.isAssignableFrom(cls)) { + String name = cls.getSimpleName(); + encoderClasses.put(name, cls); + } + } + } + + @Procedure("spatial.procedures") + @Description("Lists all spatial procedures with name and signature") + public Stream listProcedures() throws ProcedureException { + GlobalProcedures procedures = ((GraphDatabaseAPI) db).getDependencyResolver() + .resolveDependency(GlobalProcedures.class); + Stream.Builder builder = Stream.builder(); + for (ProcedureSignature proc : procedures.getCurrentView().getAllProcedures()) { + if (proc.name().namespace()[0].equals("spatial")) { + builder.accept(new NameResult(proc.name().toString(), proc.toString())); + } + } + return builder.build(); + } + + @Procedure(name = "spatial.upgrade", mode = WRITE) + @Description("Upgrades an older spatial data model and returns a list of layers upgraded") + public Stream upgradeSpatial() { + SpatialDatabaseService sdb = spatial(); + Stream.Builder builder = Stream.builder(); + for (String name : sdb.upgradeFromOldModel(tx)) { + Layer layer = sdb.getLayer(tx, name); + if (layer != null) { + builder.accept(new NameResult(name, layer.getSignature())); + } + } + return builder.build(); + } + + @Procedure(value = "spatial.layers", mode = WRITE) + @Description("Returns name, and details for all layers") + public Stream getAllLayers() { + SpatialDatabaseService sdb = spatial(); + Stream.Builder builder = Stream.builder(); + for (String name : sdb.getLayerNames(tx)) { + Layer layer = sdb.getLayer(tx, name); + if (layer != null) { + builder.accept(new NameResult(name, layer.getSignature())); + } + } + return builder.build(); + } + + @Procedure("spatial.layerTypes") + @Description("Returns the different registered layer types") + public Stream getAllLayerTypes() { + SpatialDatabaseService sdb = spatial(); + Stream.Builder builder = Stream.builder(); + for (Map.Entry entry : sdb.getRegisteredLayerTypes().entrySet()) { + builder.accept(new NameResult(entry.getKey(), entry.getValue())); + } + return builder.build(); + } + + @Procedure(value = "spatial.addPointLayer", mode = WRITE) + @Description("Adds a new simple point layer, returns the layer root node") + public Stream addSimplePointLayer( + @Name("name") String name, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), null, + selectCRS(crsName)).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addPointLayerGeohash", mode = WRITE) + @Description("Adds a new simple point layer with geohash based index, returns the layer root node") + public Stream addSimplePointLayerGeohash( + @Name("name") String name, + @Name(value = "crsName", defaultValue = WGS84_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + LayerGeohashPointIndex.class, null, + selectCRS(crsName)).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addPointLayerZOrder", mode = WRITE) + @Description("Adds a new simple point layer with z-order curve based index, returns the layer root node") + public Stream addSimplePointLayerZOrder(@Name("name") String name) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + LayerZOrderPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addPointLayerHilbert", mode = WRITE) + @Description("Adds a new simple point layer with hilbert curve based index, returns the layer root node") + public Stream addSimplePointLayerHilbert(@Name("name") String name) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + LayerHilbertPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addPointLayerXY", mode = WRITE) + @Description("Adds a new simple point layer with the given properties for x and y coordinates, returns the layer root node") + public Stream addSimplePointLayer( + @Name("name") String name, + @Name("xProperty") String xProperty, + @Name("yProperty") String yProperty, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + if (xProperty != null && yProperty != null) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), sdb.makeEncoderConfig(xProperty, yProperty), + selectCRS(hintCRSName(crsName, yProperty))).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': Missing encoder config values: xProperty[" + xProperty + + "], yProperty[" + yProperty + "]"); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addPointLayerWithConfig", mode = WRITE) + @Description("Adds a new simple point layer with the given configuration, returns the layer root node") + public Stream addSimplePointLayerWithConfig( + @Name("name") String name, + @Name("encoderConfig") String encoderConfig, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + if (encoderConfig.indexOf(':') > 0) { + return streamNode(sdb.createLayer(tx, name, SimplePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), encoderConfig, + selectCRS(hintCRSName(crsName, encoderConfig))).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': invalid encoder config '" + encoderConfig + "'"); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayer", mode = WRITE) + @Description("Adds a new native point layer, returns the layer root node") + public Stream addNativePointLayer( + @Name("name") String name, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), null, selectCRS(crsName)).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayerGeohash", mode = WRITE) + @Description("Adds a new native point layer with geohash based index, returns the layer root node") + public Stream addNativePointLayerGeohash( + @Name("name") String name, + @Name(value = "crsName", defaultValue = WGS84_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + LayerGeohashPointIndex.class, null, selectCRS(crsName)).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayerZOrder", mode = WRITE) + @Description("Adds a new native point layer with z-order curve based index, returns the layer root node") + public Stream addNativePointLayerZOrder(@Name("name") String name) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + LayerZOrderPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayerHilbert", mode = WRITE) + @Description("Adds a new native point layer with hilbert curve based index, returns the layer root node") + public Stream addNativePointLayerHilbert(@Name("name") String name) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + LayerHilbertPointIndex.class, null, DefaultGeographicCRS.WGS84).getLayerNode(tx)); + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayerXY", mode = WRITE) + @Description("Adds a new native point layer with the given properties for x and y coordinates, returns the layer root node") + public Stream addNativePointLayer( + @Name("name") String name, + @Name("xProperty") String xProperty, + @Name("yProperty") String yProperty, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + if (xProperty != null && yProperty != null) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), sdb.makeEncoderConfig(xProperty, yProperty), + selectCRS(hintCRSName(crsName, yProperty))).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': Missing encoder config values: xProperty[" + xProperty + + "], yProperty[" + yProperty + "]"); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addNativePointLayerWithConfig", mode = WRITE) + @Description("Adds a new native point layer with the given configuration, returns the layer root node") + public Stream addNativePointLayerWithConfig( + @Name("name") String name, + @Name("encoderConfig") String encoderConfig, + @Name(value = "indexType", defaultValue = RTREE_INDEX_NAME) String indexType, + @Name(value = "crsName", defaultValue = UNSET_CRS_NAME) String crsName) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + if (encoderConfig.indexOf(':') > 0) { + return streamNode(sdb.createLayer(tx, name, NativePointEncoder.class, SimplePointLayer.class, + sdb.resolveIndexClass(indexType), encoderConfig, + selectCRS(hintCRSName(crsName, encoderConfig))).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': invalid encoder config '" + encoderConfig + "'"); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + public static final String UNSET_CRS_NAME = ""; + public static final String WGS84_CRS_NAME = "wgs84"; + + /** + * Currently this only supports the string 'WGS84', for the convenience of procedure users. + * This should be expanded with CRS table lookup. + * + * @param name + * @return null or WGS84 + */ + public CoordinateReferenceSystem selectCRS(String name) { + if (name == null) { + return null; + } else { + switch (name.toLowerCase()) { + case WGS84_CRS_NAME: + return org.geotools.referencing.crs.DefaultGeographicCRS.WGS84; + case UNSET_CRS_NAME: + return null; + default: + throw new IllegalArgumentException("Unsupported CRS name: " + name); + } + } + } + + private String hintCRSName(String crsName, String hint) { + if (crsName.equals(UNSET_CRS_NAME) && hint.toLowerCase().contains("lat")) { + crsName = WGS84_CRS_NAME; + } + return crsName; + } + + @Procedure(value = "spatial.addLayerWithEncoder", mode = WRITE) + @Description("Adds a new layer with the given encoder class and configuration, returns the layer root node") + public Stream addLayerWithEncoder( + @Name("name") String name, + @Name("encoder") String encoderClassName, + @Name("encoderConfig") String encoderConfig) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + Class encoderClass = encoderClasses.get(encoderClassName); + Class layerClass = sdb.suggestLayerClassForEncoder(encoderClass); + if (encoderClass != null) { + return streamNode( + sdb.createLayer(tx, name, encoderClass, layerClass, null, encoderConfig).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': invalid encoder class '" + encoderClassName + "'"); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + @Procedure(value = "spatial.addLayer", mode = WRITE) + @Description("Adds a new layer with the given type (see spatial().getAllLayerTypes) and configuration, returns the layer root node") + public Stream addLayerOfType( + @Name("name") String name, + @Name("type") String type, + @Name("encoderConfig") String encoderConfig) { + SpatialDatabaseService sdb = spatial(); + Layer layer = sdb.getLayer(tx, name); + if (layer == null) { + Map knownTypes = sdb.getRegisteredLayerTypes(); + if (knownTypes.containsKey(type.toLowerCase())) { + return streamNode(sdb.getOrCreateRegisteredTypeLayer(tx, name, type, encoderConfig).getLayerNode(tx)); + } else { + throw new IllegalArgumentException( + "Cannot create layer '" + name + "': unknown type '" + type + "' - supported types are " + + knownTypes.toString()); + } + } else { + throw new IllegalArgumentException("Cannot create existing layer: " + name); + } + } + + private Stream streamNode(Node node) { + return Stream.of(new NodeResult(node)); + } + + private Stream streamNode(String nodeId) { + return Stream.of(new NodeIdResult(nodeId)); + } + + @Procedure(value = "spatial.addWKTLayer", mode = WRITE) + @Description("Adds a new WKT layer with the given node property to hold the WKT string, returns the layer root node") + public Stream addWKTLayer(@Name("name") String name, + @Name("nodePropertyName") String nodePropertyName) { + return addLayerOfType(name, "WKT", nodePropertyName); + } + + @Procedure(value = "spatial.layer", mode = WRITE) + @Description("Returns the layer root node for the given layer name") + public Stream getLayer(@Name("name") String name) { + return streamNode(getLayerOrThrow(tx, spatial(), name).getLayerNode(tx)); + } + + @Procedure(value = "spatial.getFeatureAttributes", mode = WRITE) + @Description("Returns feature attributes of the given layer") + public Stream getFeatureAttributes(@Name("name") String name) { + Layer layer = getLayerOrThrow(tx, spatial(), name); + return Arrays.asList(layer.getExtraPropertyNames(tx)).stream().map(StringResult::new); + } + + @Procedure(value = "spatial.setFeatureAttributes", mode = WRITE) + @Description("Sets the feature attributes of the given layer") + public Stream setFeatureAttributes(@Name("name") String name, + @Name("attributeNames") List attributeNames) { + EditableLayerImpl layer = getEditableLayerOrThrow(tx, spatial(), name); + layer.setExtraPropertyNames(attributeNames.toArray(new String[attributeNames.size()]), tx); + return streamNode(layer.getLayerNode(tx)); + } + + @Procedure(value = "spatial.removeLayer", mode = WRITE) + @Description("Removes the given layer") + public void removeLayer(@Name("name") String name) { + SpatialDatabaseService sdb = spatial(); + sdb.deleteLayer(tx, name, new ProgressLoggingListener("Deleting layer '" + name + "'", log, Level.INFO)); + } + + @Procedure(value = "spatial.addNode", mode = WRITE) + @Description("Adds the given node to the layer, returns the geometry-node") + public Stream addNodeToLayer(@Name("layerName") String name, @Name("node") Node node) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + return streamNode(layer.add(tx, node).getGeomNode()); + } + + @Procedure(value = "spatial.addNodes", mode = WRITE) + @Description("Adds the given nodes list to the layer, returns the count") + public Stream addNodesToLayer(@Name("layerName") String name, @Name("nodes") List nodes) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + return Stream.of(new CountResult(layer.addAll(tx, nodes))); + } + + @Procedure(value = "spatial.addNode.byId", mode = WRITE) + @Description("Adds the given node to the layer, returns the geometry-node") + public Stream addNodeIdToLayer(@Name("layerName") String name, @Name("nodeId") String nodeId) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + return streamNode(layer.add(tx, tx.getNodeByElementId(nodeId)).getGeomNode()); + } + + @Procedure(value = "spatial.addNodes.byId", mode = WRITE) + @Description("Adds the given nodes list to the layer, returns the count") + public Stream addNodeIdsToLayer(@Name("layerName") String name, + @Name("nodeIds") List nodeIds) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + List nodes = nodeIds.stream().map(id -> tx.getNodeByElementId(id)).collect(Collectors.toList()); + return Stream.of(new CountResult(layer.addAll(tx, nodes))); + } + + @Procedure(value = "spatial.removeNode", mode = WRITE) + @Description("Removes the given node from the layer, returns the geometry-node") + public Stream removeNodeFromLayer(@Name("layerName") String name, @Name("node") Node node) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + layer.removeFromIndex(tx, node.getElementId()); + return streamNode(node.getElementId()); + } + + @Procedure(value = "spatial.removeNodes", mode = WRITE) + @Description("Removes the given nodes from the layer, returns the count of nodes removed") + public Stream removeNodesFromLayer(@Name("layerName") String name, @Name("nodes") List nodes) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + //TODO optimize bulk node removal from RTree like we have done for node additions + int before = layer.getIndex().count(tx); + for (Node node : nodes) { + layer.removeFromIndex(tx, node.getElementId()); + } + int after = layer.getIndex().count(tx); + return Stream.of(new CountResult(before - after)); + } + + @Procedure(value = "spatial.removeNode.byId", mode = WRITE) + @Description("Removes the given node from the layer, returns the geometry-node") + public Stream removeNodeFromLayer(@Name("layerName") String name, @Name("nodeId") String nodeId) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + layer.removeFromIndex(tx, nodeId); + return streamNode(nodeId); + } + + @Procedure(value = "spatial.removeNodes.byId", mode = WRITE) + @Description("Removes the given nodes from the layer, returns the count of nodes removed") + public Stream removeNodeIdsFromLayer(@Name("layerName") String name, + @Name("nodeIds") List nodeIds) { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + //TODO optimize bulk node removal from RTree like we have done for node additions + int before = layer.getIndex().count(tx); + for (String nodeId : nodeIds) { + layer.removeFromIndex(tx, nodeId); + } + int after = layer.getIndex().count(tx); + return Stream.of(new CountResult(before - after)); + } + + @Procedure(value = "spatial.addWKT", mode = WRITE) + @Description("Adds the given WKT string to the layer, returns the created geometry node") + public Stream addGeometryWKTToLayer(@Name("layerName") String name, + @Name("geometry") String geometryWKT) throws ParseException { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + WKTReader reader = new WKTReader(layer.getGeometryFactory()); + return streamNode(addGeometryWkt(layer, reader, geometryWKT)); + } + + @Procedure(value = "spatial.addWKTs", mode = WRITE) + @Description("Adds the given WKT string list to the layer, returns the created geometry nodes") + public Stream addGeometryWKTsToLayer(@Name("layerName") String name, + @Name("geometry") List geometryWKTs) throws ParseException { + EditableLayer layer = getEditableLayerOrThrow(tx, spatial(), name); + WKTReader reader = new WKTReader(layer.getGeometryFactory()); + return geometryWKTs.stream().map(geometryWKT -> addGeometryWkt(layer, reader, geometryWKT)) + .map(NodeResult::new); + } + + private Node addGeometryWkt(EditableLayer layer, WKTReader reader, String geometryWKT) { + try { + Geometry geometry = reader.read(geometryWKT); + return layer.add(tx, geometry).getGeomNode(); + } catch (ParseException e) { + throw new RuntimeException("Error parsing geometry: " + geometryWKT, e); + } + } + + @Procedure(value = "spatial.importShapefileToLayer", mode = WRITE) + @Description("Imports the the provided shape-file from URI to the given layer, returns the count of data added") + public Stream importShapefile( + @Name("layerName") String name, + @Name("uri") String uri) throws IOException { + EditableLayerImpl layer = getEditableLayerOrThrow(tx, spatial(), name); + return Stream.of(new CountResult(importShapefileToLayer(uri, layer, 1000).size())); + } + + @Procedure(value = "spatial.importShapefile", mode = WRITE) + @Description("Imports the the provided shape-file from URI to a layer of the same name, returns the count of data added") + public Stream importShapefile( + @Name("uri") String uri) throws IOException { + return Stream.of(new CountResult(importShapefileToLayer(uri, null, 1000).size())); + } + + private List importShapefileToLayer(String shpPath, EditableLayerImpl layer, int commitInterval) + throws IOException { + if (shpPath.toLowerCase().endsWith(".shp")) { + // remove extension + shpPath = shpPath.substring(0, shpPath.lastIndexOf(".")); + } + + ShapefileImporter importer = new ShapefileImporter(db, + new ProgressLoggingListener("Importing " + shpPath, log, Level.DEBUG), commitInterval); + if (layer == null) { + String layerName = shpPath.substring(shpPath.lastIndexOf(File.separator) + 1); + return importer.importFile(shpPath, layerName); + } else { + return importer.importFile(shpPath, layer, Charset.defaultCharset()); + } + } + + @Procedure(value = "spatial.importOSMToLayer", mode = WRITE) + @Description("Imports the the provided osm-file from URI to a layer, returns the count of data added") + public Stream importOSM( + @Name("layerName") String layerName, + @Name("uri") String uri) throws InterruptedException { + // Delegate finding the layer to the inner thread, so we do not pollute the procedure transaction with anything that might conflict. + // Since the procedure transaction starts before, and ends after, all inner transactions. + BiFunction layerFinder = (tx, name) -> (OSMLayer) getEditableLayerOrThrow(tx, + spatial(), name); + return Stream.of(new CountResult(importOSMToLayer(uri, layerName, layerFinder))); + } + + @Procedure(value = "spatial.importOSM", mode = WRITE) + @Description("Imports the the provided osm-file from URI to a layer of the same name, returns the count of data added") + public Stream importOSM( + @Name("uri") String uri) throws InterruptedException { + String layerName = uri.substring(uri.lastIndexOf(File.separator) + 1); + assertLayerDoesNotExists(tx, spatial(), layerName); + // Delegate creating the layer to the inner thread, so we do not pollute the procedure transaction with anything that might conflict. + // Since the procedure transaction starts before, and ends after, all inner transactions. + BiFunction layerMaker = (tx, name) -> (OSMLayer) spatial().getOrCreateLayer(tx, + name, OSMGeometryEncoder.class, OSMLayer.class); + return Stream.of(new CountResult(importOSMToLayer(uri, layerName, layerMaker))); + } + + private long importOSMToLayer(String osmPath, String layerName, + BiFunction layerMaker) throws InterruptedException { + if (!osmPath.toLowerCase().endsWith(".osm")) { + // add extension + osmPath = osmPath + ".osm"; + } + OSMImportRunner runner = new OSMImportRunner(api, ktx.securityContext(), osmPath, layerName, layerMaker, log, + Level.DEBUG); + Thread importerThread = new Thread(runner); + importerThread.start(); + importerThread.join(); + return runner.getResult(); + } + + private static class OSMImportRunner implements Runnable { + + private final GraphDatabaseAPI db; + private final String osmPath; + private final String layerName; + private final BiFunction layerMaker; + private final Log log; + private final Level level; + private final SecurityContext securityContext; + private Exception e; + private long rc = -1; + + OSMImportRunner(GraphDatabaseAPI db, SecurityContext securityContext, String osmPath, String layerName, + BiFunction layerMaker, Log log, Level level) { + this.db = db; + this.osmPath = osmPath; + this.layerName = layerName; + this.layerMaker = layerMaker; + this.log = log; + this.level = level; + this.securityContext = securityContext; + } + + long getResult() { + if (e == null) { + return rc; + } else { + throw new RuntimeException( + "Failed to import " + osmPath + " to layer '" + layerName + "': " + e.getMessage(), e); + } + } + + @Override + public void run() { + // Create the layer in the same thread as doing the import, otherwise we have an outer thread doing a create, + // and the inner thread repeating it, resulting in duplicates + try (Transaction tx = db.beginTransaction(KernelTransaction.Type.EXPLICIT, securityContext)) { + layerMaker.apply(tx, layerName); + tx.commit(); + } + OSMImporter importer = new OSMImporter(layerName, + new ProgressLoggingListener("Importing " + osmPath, log, level)); + try { + // Provide the security context for all inner transactions that will be made during import + importer.setSecurityContext(securityContext); + // import using multiple, serial inner transactions (using the security context of the outer thread) + importer.importFile(db, osmPath, false, 10000); + // Re-index using inner transactions (using the security context of the outer thread) + rc = importer.reIndex(db, 10000, false); + } catch (Exception e) { + log.error("Error running OSMImporter: " + e.getMessage()); + this.e = e; + } + } + } + + @Procedure(value = "spatial.bbox", mode = WRITE) + @Description("Finds all geometry nodes in the given layer within the lower left and upper right coordinates of a box") + public Stream findGeometriesInBBox( + @Name("layerName") String name, + @Name("min") Object min, + @Name("max") Object max) { + Layer layer = getLayerOrThrow(tx, spatial(), name); + // TODO why a SearchWithin and not a SearchIntersectWindow? + Envelope envelope = new Envelope(toCoordinate(min), toCoordinate(max)); + return GeoPipeline + .startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(envelope)) + .stream().map(GeoPipeFlow::getGeomNode).map(NodeResult::new); + } + + @Procedure(value = "spatial.closest", mode = WRITE) + @Description("Finds all geometry nodes in the layer within the distance to the given coordinate") + public Stream findClosestGeometries( + @Name("layerName") String name, + @Name("coordinate") Object coordinate, + @Name("distanceInKm") double distanceInKm) { + Layer layer = getLayerOrThrow(tx, spatial(), name); + GeometryFactory factory = layer.getGeometryFactory(); + Point point = factory.createPoint(toCoordinate(coordinate)); + List edgeResults = SpatialTopologyUtils.findClosestEdges(tx, point, layer, + distanceInKm); + return edgeResults.stream().map(e -> e.getValue().getGeomNode()).map(NodeResult::new); + } + + @Procedure(value = "spatial.withinDistance", mode = WRITE) + @Description("Returns all geometry nodes and their ordered distance in the layer within the distance to the given coordinate") + public Stream findGeometriesWithinDistance( + @Name("layerName") String name, + @Name("coordinate") Object coordinate, + @Name("distanceInKm") double distanceInKm) { + + Layer layer = getLayerOrThrow(tx, spatial(), name); + return GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, toCoordinate(coordinate), distanceInKm) + .sort(OrthodromicDistance.DISTANCE) + .stream().map(r -> { + double distance = r.hasProperty(tx, OrthodromicDistance.DISTANCE) ? ((Number) r.getProperty(tx, + OrthodromicDistance.DISTANCE)).doubleValue() : -1; + return new NodeDistanceResult(r.getGeomNode(), distance); + }); + } + + @UserFunction("spatial.decodeGeometry") + @Description("Returns a geometry of a layer node as the Neo4j geometry type, to be passed to other procedures or returned to a client") + public Object decodeGeometry( + @Name("layerName") String name, + @Name("node") Node node) { + + Layer layer = getLayerOrThrow(tx, spatial(), name); + GeometryResult result = new GeometryResult( + toNeo4jGeometry(layer, layer.getGeometryEncoder().decodeGeometry(node))); + return result.geometry; + } + + @UserFunction("spatial.asMap") + @Description("Returns a Map object representing the Geometry, to be passed to other procedures or returned to a client") + public Object asMap(@Name("object") Object geometry) { + return toGeometryMap(geometry); + } + + @UserFunction("spatial.asGeometry") + @Description("Returns a geometry object as the Neo4j geometry type, to be passed to other functions or procedures or returned to a client") + public Object asGeometry( + @Name("geometry") Object geometry) { + + return toNeo4jGeometry(null, geometry); + } + + @Deprecated + @Procedure("spatial.asGeometry") + @Description("Returns a geometry object as the Neo4j geometry type, to be passed to other procedures or returned to a client") + public Stream asGeometryProc( + @Name("geometry") Object geometry) { + + return Stream.of(geometry).map(geom -> new GeometryResult(toNeo4jGeometry(null, geom))); + } + + @Deprecated + @Procedure(value = "spatial.asExternalGeometry", deprecatedBy = "spatial.asGeometry") + @Description("Returns a geometry object as an external geometry type to be returned to a client") + // This only existed temporarily because the other method, asGeometry, returned the wrong type due to a bug in Neo4j 3.0 + public Stream asExternalGeometry( + @Name("geometry") Object geometry) { + + return Stream.of(geometry).map(geom -> new GeometryResult(toNeo4jGeometry(null, geom))); + } + + @Procedure(value = "spatial.intersects", mode = WRITE) + @Description("Returns all geometry nodes that intersect the given geometry (shape, polygon) in the layer") + public Stream findGeometriesIntersecting( + @Name("layerName") String name, + @Name("geometry") Object geometry) { + + Layer layer = getLayerOrThrow(tx, spatial(), name); + return GeoPipeline + .startIntersectSearch(tx, layer, toJTSGeometry(layer, geometry)) + .stream().map(GeoPipeFlow::getGeomNode).map(NodeResult::new); + } + + private Geometry toJTSGeometry(Layer layer, Object value) { + GeometryFactory factory = layer.getGeometryFactory(); + if (value instanceof org.neo4j.graphdb.spatial.Point) { + org.neo4j.graphdb.spatial.Point point = (org.neo4j.graphdb.spatial.Point) value; + double[] coord = point.getCoordinate().getCoordinate(); + return factory.createPoint(new Coordinate(coord[0], coord[1])); + } + if (value instanceof String) { + WKTReader reader = new WKTReader(factory); + try { + return reader.read((String) value); + } catch (ParseException e) { + throw new IllegalArgumentException("Invalid WKT: " + e.getMessage()); + } + } + Map latLon = null; + if (value instanceof Entity) { + latLon = ((Entity) value).getProperties("latitude", "longitude", "lat", "lon"); + } + if (value instanceof Map) { + latLon = (Map) value; + } + Coordinate coord = toCoordinate(latLon); + if (coord != null) { + return factory.createPoint(coord); + } + throw new RuntimeException("Can't convert " + value + " to a geometry"); + } + + private static org.neo4j.graphdb.spatial.Coordinate toNeo4jCoordinate(Coordinate coordinate) { + if (coordinate.z == Coordinate.NULL_ORDINATE) { + return new org.neo4j.graphdb.spatial.Coordinate(coordinate.x, coordinate.y); + } else { + return new org.neo4j.graphdb.spatial.Coordinate(coordinate.x, coordinate.y, coordinate.z); + } + } + + private static List toNeo4jCoordinates(Coordinate[] coordinates) { + ArrayList converted = new ArrayList<>(); + for (Coordinate coordinate : coordinates) { + converted.add(toNeo4jCoordinate(coordinate)); + } + return converted; + } + + private org.neo4j.graphdb.spatial.Geometry toNeo4jGeometry(Layer layer, Object value) { + if (value instanceof org.neo4j.graphdb.spatial.Geometry) { + return (org.neo4j.graphdb.spatial.Geometry) value; + } + Neo4jCRS crs = findCRS("Cartesian"); + if (layer != null) { + CoordinateReferenceSystem layerCRS = layer.getCoordinateReferenceSystem(tx); + if (layerCRS != null) { + ReferenceIdentifier crsRef = layer.getCoordinateReferenceSystem(tx).getName(); + crs = findCRS(crsRef.toString()); + } + } + if (value instanceof Point) { + Point point = (Point) value; + return new Neo4jPoint(point, crs); + } + if (value instanceof Geometry) { + Geometry geometry = (Geometry) value; + return new Neo4jGeometry(geometry.getGeometryType(), toNeo4jCoordinates(geometry.getCoordinates()), crs); + } + if (value instanceof String && layer != null) { + GeometryFactory factory = layer.getGeometryFactory(); + WKTReader reader = new WKTReader(factory); + try { + Geometry geometry = reader.read((String) value); + return new Neo4jGeometry(geometry.getGeometryType(), toNeo4jCoordinates(geometry.getCoordinates()), + crs); + } catch (ParseException e) { + throw new IllegalArgumentException("Invalid WKT: " + e.getMessage()); + } + } + Map latLon = null; + if (value instanceof Entity) { + latLon = ((Entity) value).getProperties("latitude", "longitude", "lat", "lon"); + } + if (value instanceof Map) { + latLon = (Map) value; + } + Coordinate coord = toCoordinate(latLon); + if (coord != null) { + return new Neo4jPoint(coord, crs); + } + throw new RuntimeException("Can't convert " + value + " to a geometry"); + } + + private Object toPublic(Object obj) { + if (obj instanceof Map) { + return toPublic((Map) obj); + } else if (obj instanceof Entity) { + return toPublic(((Entity) obj).getProperties()); + } else if (obj instanceof Geometry) { + return toMap((Geometry) obj); + } else { + return obj; + } + } + + private Map toGeometryMap(Object geometry) { + return toMap(toNeo4jGeometry(null, geometry)); + } + + private Map toMap(Geometry geometry) { + return toMap(toNeo4jGeometry(null, geometry)); + } + + private static double[][] toCoordinateArrayFromCoordinates(List coords) { + List coordinates = new ArrayList<>(coords.size()); + for (org.neo4j.graphdb.spatial.Coordinate coord : coords) { + coordinates.add(coord.getCoordinate()); + } + return toCoordinateArray(coordinates); + } + + private static double[][] toCoordinateArray(List coords) { + double[][] coordinates = new double[coords.size()][]; + for (int i = 0; i < coordinates.length; i++) { + coordinates[i] = coords.get(i); + } + return coordinates; + } + + private static Map toMap(org.neo4j.graphdb.spatial.Geometry geometry) { + if (geometry instanceof org.neo4j.graphdb.spatial.Point) { + org.neo4j.graphdb.spatial.Point point = (org.neo4j.graphdb.spatial.Point) geometry; + return map("type", geometry.getGeometryType(), "coordinate", point.getCoordinate().getCoordinate()); + } else { + return map("type", geometry.getGeometryType(), "coordinates", + toCoordinateArrayFromCoordinates(geometry.getCoordinates())); + } + } + + private Map toPublic(Map incoming) { + Map map = new HashMap<>(incoming.size()); + for (Object key : incoming.keySet()) { + map.put(key.toString(), toPublic(incoming.get(key))); + } + return map; + } + + private Coordinate toCoordinate(Object value) { + if (value instanceof Coordinate) { + return (Coordinate) value; + } + if (value instanceof org.neo4j.graphdb.spatial.Coordinate) { + return toCoordinate((org.neo4j.graphdb.spatial.Coordinate) value); + } + if (value instanceof org.neo4j.graphdb.spatial.Point) { + return toCoordinate(((org.neo4j.graphdb.spatial.Point) value).getCoordinate()); + } + if (value instanceof Entity) { + return toCoordinate(((Entity) value).getProperties("latitude", "longitude", "lat", "lon")); + } + if (value instanceof Map) { + return toCoordinate((Map) value); + } + throw new RuntimeException("Can't convert " + value + " to a coordinate"); + } + + private static Coordinate toCoordinate(org.neo4j.graphdb.spatial.Coordinate point) { + double[] coordinate = point.getCoordinate(); + return new Coordinate(coordinate[0], coordinate[1]); + } + + private static Coordinate toCoordinate(Map map) { + if (map == null) { + return null; + } + Coordinate coord = toCoordinate(map, "longitude", "latitude"); + if (coord == null) { + return toCoordinate(map, "lon", "lat"); + } + return coord; + } + + private static Coordinate toCoordinate(Map map, String xName, String yName) { + if (map.containsKey(xName) && map.containsKey(yName)) { + return new Coordinate(((Number) map.get(xName)).doubleValue(), ((Number) map.get(yName)).doubleValue()); + } + return null; + } + + private static EditableLayerImpl getEditableLayerOrThrow(Transaction tx, SpatialDatabaseService spatial, + String name) { + return (EditableLayerImpl) getLayerOrThrow(tx, spatial, name); + } + + private static Layer getLayerOrThrow(Transaction tx, SpatialDatabaseService spatial, String name) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, name); + if (layer != null) { + return layer; + } else { + throw new IllegalArgumentException("No such layer '" + name + "'"); + } + } + + private static void assertLayerDoesNotExists(Transaction tx, SpatialDatabaseService spatial, String name) { + if (spatial.getLayer(tx, name) != null) { + throw new IllegalArgumentException("Layer already exists: '" + name + "'"); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/process/SpatialProcess.java b/src/main/java/org/neo4j/gis/spatial/process/SpatialProcess.java index 38c2d8958..14eda275f 100644 --- a/src/main/java/org/neo4j/gis/spatial/process/SpatialProcess.java +++ b/src/main/java/org/neo4j/gis/spatial/process/SpatialProcess.java @@ -24,7 +24,6 @@ import org.geotools.process.factory.DescribeResult; import org.geotools.process.factory.StaticMethodsProcessFactory; import org.geotools.text.Text; - import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.OctagonalEnvelope; @@ -35,8 +34,8 @@ public SpatialProcess() { } @DescribeProcess(title = "Octagonal Envelope", description = "Get the octagonal envelope of this Geometry.") - @DescribeResult(description="octagonal of geom") + @DescribeResult(description = "octagonal of geom") static public Geometry octagonalEnvelope(@DescribeParameter(name = "geom") Geometry geom) { - return new OctagonalEnvelope(geom).toGeometry(geom.getFactory()); + return new OctagonalEnvelope(geom).toGeometry(geom.getFactory()); } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/EmptyMonitor.java b/src/main/java/org/neo4j/gis/spatial/rtree/EmptyMonitor.java index 7cc8f18af..d5ecb56ec 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/EmptyMonitor.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/EmptyMonitor.java @@ -20,81 +20,73 @@ package org.neo4j.gis.spatial.rtree; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Transaction; - import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; + +public class EmptyMonitor implements TreeMonitor { -public class EmptyMonitor implements TreeMonitor -{ - @Override - public void setHeight(int height) - { - } + @Override + public void setHeight(int height) { + } - public int getHeight() - { - return -1; - } + public int getHeight() { + return -1; + } - @Override - public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) - { - } + @Override + public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) { + } - @Override - public int getNbrRebuilt() - { - return -1; - } + @Override + public int getNbrRebuilt() { + return -1; + } - @Override - public void addSplit(Node indexNode) - { + @Override + public void addSplit(Node indexNode) { - } + } - @Override - public void beforeMergeTree(Node indexNode, List right) { + @Override + public void beforeMergeTree(Node indexNode, List right) { - } + } - @Override - public void afterMergeTree(Node indexNode) { + @Override + public void afterMergeTree(Node indexNode) { - } + } - @Override - public int getNbrSplit() - { - return -1; - } + @Override + public int getNbrSplit() { + return -1; + } - @Override - public void addCase(String key) { + @Override + public void addCase(String key) { - } + } - @Override - public Map getCaseCounts() { - return null; - } + @Override + public Map getCaseCounts() { + return null; + } - @Override - public void reset() - { + @Override + public void reset() { - } + } - @Override - public void matchedTreeNode(int level, Node node) { + @Override + public void matchedTreeNode(int level, Node node) { - } + } - @Override - public List getMatchedTreeNodes(int level) { - return new ArrayList(); - } + @Override + public List getMatchedTreeNodes(int level) { + return new ArrayList(); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/Envelope.java b/src/main/java/org/neo4j/gis/spatial/rtree/Envelope.java index 17f6f0245..9703e046b 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/Envelope.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/Envelope.java @@ -21,119 +21,124 @@ public class Envelope extends org.neo4j.gis.spatial.index.Envelope { - /** - * Copy constructor - */ - public Envelope(org.neo4j.gis.spatial.index.Envelope e) { - super(e.getMin(), e.getMax()); - } - - /** - * General constructor for the n-dimensional case - */ - public Envelope(double[] min, double[] max) { - super(min, max); - } - - /** - * General constructor for the n-dimensional case starting with a single point - */ - public Envelope(double[] p) { - super(p.clone(), p.clone()); - } - - /** - * Special constructor for the 2D case - */ - public Envelope(double xmin, double xmax, double ymin, double ymax) { - super(xmin, xmax, ymin, ymax); - } - - /** - * Note that this doesn't exclude the envelope boundary. - * See JTS Envelope. - */ - public boolean contains(Envelope other) { - //TODO: We can remove this method and covers method if we determine why super.covers does not do boolean shortcut - return covers(other); - } - - public boolean covers(Envelope other) { - boolean ans = getDimension() == other.getDimension(); - for (int i = 0; i < min.length; i++) { - //TODO: Why does the parent class not use this shortcut? - if (!ans) - return ans; - ans = ans && other.min[i] >= min[i] && other.max[i] <= max[i]; - } - return ans; - } - - public void scaleBy(double factor) { - for (int i = 0; i < min.length; i++) { - scaleBy(factor, i); - } - } - - private void scaleBy(double factor, int dimension) { - max[dimension] = min[dimension] + (max[dimension] - min[dimension]) * factor; - } - - public void shiftBy(double offset) { - for (int i = 0; i < min.length; i++) { - shiftBy(offset, i); - } - } - - public void shiftBy(double offset, int dimension) { - min[dimension] += offset; - max[dimension] += offset; - } - - public double[] centre() { - double[] center = new double[min.length]; - for (int i = 0; i < min.length; i++) { - center[i] = centre(i); - } - return center; - } - - public double centre(int dimension) { - return (min[dimension] + max[dimension]) / 2.0; - } - - public void expandToInclude(double[] p) { - for (int i = 0; i < Math.min(p.length, min.length); i++) { - if (p[i] < min[i]) - min[i] = p[i]; - if (p[i] > max[i]) - max[i] = p[i]; - } - } - - public double separation(Envelope other) { - Envelope combined = new Envelope(this); - combined.expandToInclude(other); - return combined.getArea() - this.getArea() - other.getArea(); - } - - public double separation(Envelope other, int dimension) { - Envelope combined = new Envelope(this); - combined.expandToInclude(other); - return combined.getWidth(dimension) - this.getWidth(dimension) - other.getWidth(dimension); - } - - public Envelope intersection(Envelope other) { - return new Envelope(super.intersection(other)); - } - - public Envelope bbox(Envelope other) { - if (getDimension() == other.getDimension()) { - Envelope result = new Envelope(this); - result.expandToInclude(other); - return result; - } else { - throw new IllegalArgumentException("Cannot calculate bounding box of Envelopes with different dimensions: " + this.getDimension() + " != " + other.getDimension()); - } - } + /** + * Copy constructor + */ + public Envelope(org.neo4j.gis.spatial.index.Envelope e) { + super(e.getMin(), e.getMax()); + } + + /** + * General constructor for the n-dimensional case + */ + public Envelope(double[] min, double[] max) { + super(min, max); + } + + /** + * General constructor for the n-dimensional case starting with a single point + */ + public Envelope(double[] p) { + super(p.clone(), p.clone()); + } + + /** + * Special constructor for the 2D case + */ + public Envelope(double xmin, double xmax, double ymin, double ymax) { + super(xmin, xmax, ymin, ymax); + } + + /** + * Note that this doesn't exclude the envelope boundary. + * See JTS Envelope. + */ + public boolean contains(Envelope other) { + //TODO: We can remove this method and covers method if we determine why super.covers does not do boolean shortcut + return covers(other); + } + + public boolean covers(Envelope other) { + boolean ans = getDimension() == other.getDimension(); + for (int i = 0; i < min.length; i++) { + //TODO: Why does the parent class not use this shortcut? + if (!ans) { + return ans; + } + ans = ans && other.min[i] >= min[i] && other.max[i] <= max[i]; + } + return ans; + } + + public void scaleBy(double factor) { + for (int i = 0; i < min.length; i++) { + scaleBy(factor, i); + } + } + + private void scaleBy(double factor, int dimension) { + max[dimension] = min[dimension] + (max[dimension] - min[dimension]) * factor; + } + + public void shiftBy(double offset) { + for (int i = 0; i < min.length; i++) { + shiftBy(offset, i); + } + } + + public void shiftBy(double offset, int dimension) { + min[dimension] += offset; + max[dimension] += offset; + } + + public double[] centre() { + double[] center = new double[min.length]; + for (int i = 0; i < min.length; i++) { + center[i] = centre(i); + } + return center; + } + + public double centre(int dimension) { + return (min[dimension] + max[dimension]) / 2.0; + } + + public void expandToInclude(double[] p) { + for (int i = 0; i < Math.min(p.length, min.length); i++) { + if (p[i] < min[i]) { + min[i] = p[i]; + } + if (p[i] > max[i]) { + max[i] = p[i]; + } + } + } + + public double separation(Envelope other) { + Envelope combined = new Envelope(this); + combined.expandToInclude(other); + return combined.getArea() - this.getArea() - other.getArea(); + } + + public double separation(Envelope other, int dimension) { + Envelope combined = new Envelope(this); + combined.expandToInclude(other); + return combined.getWidth(dimension) - this.getWidth(dimension) - other.getWidth(dimension); + } + + public Envelope intersection(Envelope other) { + return new Envelope(super.intersection(other)); + } + + public Envelope bbox(Envelope other) { + if (getDimension() == other.getDimension()) { + Envelope result = new Envelope(this); + result.expandToInclude(other); + return result; + } else { + throw new IllegalArgumentException( + "Cannot calculate bounding box of Envelopes with different dimensions: " + this.getDimension() + + " != " + other.getDimension()); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoder.java b/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoder.java index 70e42d60e..8aa634eed 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoder.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoder.java @@ -24,6 +24,6 @@ public interface EnvelopeDecoder { - Envelope decodeEnvelope(Entity container); + Envelope decodeEnvelope(Entity container); -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoderFromDoubleArray.java b/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoderFromDoubleArray.java index ae48f1798..de4301f47 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoderFromDoubleArray.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/EnvelopeDecoderFromDoubleArray.java @@ -23,7 +23,6 @@ /** - * * The property must contain an array of double: xmin, ymin, xmax, ymax. */ public class EnvelopeDecoderFromDoubleArray implements EnvelopeDecoder { @@ -31,22 +30,22 @@ public class EnvelopeDecoderFromDoubleArray implements EnvelopeDecoder { public EnvelopeDecoderFromDoubleArray(String propertyName) { this.propertyName = propertyName; } - - @Override + + @Override public Envelope decodeEnvelope(Entity container) { - Object propValue = container.getProperty(propertyName); - - if (propValue instanceof Double[]) { - Double[] bbox = (Double[]) propValue; + Object propValue = container.getProperty(propertyName); + + if (propValue instanceof Double[]) { + Double[] bbox = (Double[]) propValue; return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); } else if (propValue instanceof double[]) { double[] bbox = (double[]) propValue; return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); - } else { - // invalid content - return new Envelope(new double[0]); - } + } else { + // invalid content + return new Envelope(new double[0]); + } } private String propertyName; -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/Listener.java b/src/main/java/org/neo4j/gis/spatial/rtree/Listener.java index d5377966b..773725309 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/Listener.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/Listener.java @@ -21,8 +21,8 @@ /** - * Classes that implement this interface will be notified of units of work done, - * and can therefor be used for progress bars or console logging or similar activities. + * Classes that implement this interface will be notified of units of work done, + * and can therefor be used for progress bars or console logging or similar activities. */ public interface Listener { @@ -32,4 +32,4 @@ public interface Listener { void done(); -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/NullListener.java b/src/main/java/org/neo4j/gis/spatial/rtree/NullListener.java index a11984d66..77bd717b4 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/NullListener.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/NullListener.java @@ -25,18 +25,18 @@ */ public class NullListener implements Listener { - // Public methods + // Public methods @Override - public void begin(int unitsOfWork) { - } - + public void begin(int unitsOfWork) { + } + @Override - public void worked(int workedSinceLastNotification) { - } - + public void worked(int workedSinceLastNotification) { + } + @Override - public void done() { - } + public void done() { + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/ProgressLoggingListener.java b/src/main/java/org/neo4j/gis/spatial/rtree/ProgressLoggingListener.java index ce4b1d07d..1ef2543f9 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/ProgressLoggingListener.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/ProgressLoggingListener.java @@ -19,11 +19,10 @@ */ package org.neo4j.gis.spatial.rtree; -import org.neo4j.logging.Level; -import org.neo4j.logging.Log; - import java.io.PrintStream; import java.util.Locale; +import org.neo4j.logging.Level; +import org.neo4j.logging.Log; /** * This listener logs percentage progress to the specified PrintStream or Logger based on a timer, @@ -31,91 +30,92 @@ */ public class ProgressLoggingListener implements Listener { - private final ProgressLog out; - private final String name; - private long lastLogTime = 0L; - private int totalUnits = 0; - private int workedSoFar = 0; - private boolean enabled = false; - private long timeWait = 1000; + private final ProgressLog out; + private final String name; + private long lastLogTime = 0L; + private int totalUnits = 0; + private int workedSoFar = 0; + private boolean enabled = false; + private long timeWait = 1000; + + public interface ProgressLog { - public interface ProgressLog { - void log(String line); - } + void log(String line); + } - public ProgressLoggingListener(String name, final PrintStream out) { - this.name = name; - this.out = line -> out.println(line); - } + public ProgressLoggingListener(String name, final PrintStream out) { + this.name = name; + this.out = line -> out.println(line); + } - public ProgressLoggingListener(String name, Log log, Level level) { - this.name = name; - this.out = line -> - { - switch (level) { - case DEBUG: - log.debug(line); - case ERROR: - log.error(line); - case INFO: - log.info(line); - case WARN: - log.warn(line); - default: - break; - } - }; - } + public ProgressLoggingListener(String name, Log log, Level level) { + this.name = name; + this.out = line -> + { + switch (level) { + case DEBUG: + log.debug(line); + case ERROR: + log.error(line); + case INFO: + log.info(line); + case WARN: + log.warn(line); + default: + break; + } + }; + } - public ProgressLoggingListener setTimeWait(long ms) { - this.timeWait = ms; - return this; - } + public ProgressLoggingListener setTimeWait(long ms) { + this.timeWait = ms; + return this; + } @Override - public void begin(int unitsOfWork) { - this.totalUnits = unitsOfWork; - this.workedSoFar = 0; - this.lastLogTime = 0L; - try { - this.enabled = true; - out.log("Starting " + name); - } catch (Exception e) { - System.err.println("Failed to write to output - disabling progress logger: " + e.getMessage()); - this.enabled = false; - } - } + public void begin(int unitsOfWork) { + this.totalUnits = unitsOfWork; + this.workedSoFar = 0; + this.lastLogTime = 0L; + try { + this.enabled = true; + out.log("Starting " + name); + } catch (Exception e) { + System.err.println("Failed to write to output - disabling progress logger: " + e.getMessage()); + this.enabled = false; + } + } @Override - public void worked(int workedSinceLastNotification) { - this.workedSoFar += workedSinceLastNotification; - logNoMoreThanOnceASecond("Running"); - } + public void worked(int workedSinceLastNotification) { + this.workedSoFar += workedSinceLastNotification; + logNoMoreThanOnceASecond("Running"); + } @Override - public void done() { - this.workedSoFar = this.totalUnits; - this.lastLogTime = 0L; - logNoMoreThanOnceASecond("Completed"); - } + public void done() { + this.workedSoFar = this.totalUnits; + this.lastLogTime = 0L; + logNoMoreThanOnceASecond("Completed"); + } - private void logNoMoreThanOnceASecond(String action) { - long now = System.currentTimeMillis(); - if (enabled && now - lastLogTime > timeWait) { - if (totalUnits > 0) { - out.log("" + percText() + " (" + workedSoFar + "/" + totalUnits + ") - " + action + " " + name); - } else { - out.log(action + " " + name); - } - this.lastLogTime = now; - } - } + private void logNoMoreThanOnceASecond(String action) { + long now = System.currentTimeMillis(); + if (enabled && now - lastLogTime > timeWait) { + if (totalUnits > 0) { + out.log("" + percText() + " (" + workedSoFar + "/" + totalUnits + ") - " + action + " " + name); + } else { + out.log(action + " " + name); + } + this.lastLogTime = now; + } + } - private String percText() { - if (totalUnits > 0) { - return String.format(Locale.ENGLISH,"%.2f", 100.0 * workedSoFar / totalUnits); - } else { - return "NaN"; - } - } + private String percText() { + if (totalUnits > 0) { + return String.format(Locale.ENGLISH, "%.2f", 100.0 * workedSoFar / totalUnits); + } else { + return "NaN"; + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeImageExporter.java b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeImageExporter.java index b8b138350..b8081e30e 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeImageExporter.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeImageExporter.java @@ -19,261 +19,276 @@ */ package org.neo4j.gis.spatial.rtree; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import javax.imageio.ImageIO; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; +import org.geotools.api.style.Style; import org.geotools.data.memory.MemoryFeatureCollection; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.data.neo4j.StyledImageExporter; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.MapContent; import org.geotools.renderer.lite.StreamingRenderer; -import org.geotools.api.style.Style; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.neo4j.gis.spatial.Constants; import org.neo4j.gis.spatial.GeometryEncoder; import org.neo4j.gis.spatial.SpatialTopologyUtils; import org.neo4j.gis.spatial.Utilities; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; - -import static java.awt.RenderingHints.*; public class RTreeImageExporter { - private CoordinateReferenceSystem crs; - private File exportDir; - double zoom = 0.98; - double[] offset = new double[]{0, 0}; - Rectangle displaySize = new Rectangle(2160, 2160); - private RTreeIndex index; - private GeometryFactory geometryFactory; - private GeometryEncoder geometryEncoder; - private SimpleFeatureType featureType; - private final Color[] colors = new Color[]{Color.BLUE, Color.CYAN, Color.GREEN, Color.RED, Color.YELLOW, Color.PINK, Color.ORANGE}; - ReferencedEnvelope bounds; - public RTreeImageExporter(GeometryFactory geometryFactory, GeometryEncoder geometryEncoder, CoordinateReferenceSystem crs, SimpleFeatureType featureType, RTreeIndex index) { - this.geometryFactory = geometryFactory; - this.geometryEncoder = geometryEncoder; - this.featureType = featureType; - this.index = index; - this.crs = crs; - this.bounds = new ReferencedEnvelope(crs); - } + private CoordinateReferenceSystem crs; + private File exportDir; + double zoom = 0.98; + double[] offset = new double[]{0, 0}; + Rectangle displaySize = new Rectangle(2160, 2160); + private RTreeIndex index; + private GeometryFactory geometryFactory; + private GeometryEncoder geometryEncoder; + private SimpleFeatureType featureType; + private final Color[] colors = new Color[]{Color.BLUE, Color.CYAN, Color.GREEN, Color.RED, Color.YELLOW, Color.PINK, + Color.ORANGE}; + ReferencedEnvelope bounds; - public void initialize(Transaction tx) { - Envelope bbox = index.getBoundingBox(tx); - if (bbox != null) { - bounds.expandToInclude(Utilities.fromNeo4jToJts(bbox)); - } - bounds = SpatialTopologyUtils.adjustBounds(bounds, 1.0 / zoom, offset); - } + public RTreeImageExporter(GeometryFactory geometryFactory, GeometryEncoder geometryEncoder, + CoordinateReferenceSystem crs, SimpleFeatureType featureType, RTreeIndex index) { + this.geometryFactory = geometryFactory; + this.geometryEncoder = geometryEncoder; + this.featureType = featureType; + this.index = index; + this.crs = crs; + this.bounds = new ReferencedEnvelope(crs); + } - public void initialize(Transaction tx, Coordinate min, Coordinate max) { - Envelope bbox = index.getBoundingBox(tx); - if (bbox != null) { - bounds.expandToInclude(Utilities.fromNeo4jToJts(bbox)); - } - bounds.expandToInclude(new org.locationtech.jts.geom.Envelope(min.x, max.x, min.y, max.y)); - bounds = SpatialTopologyUtils.adjustBounds(bounds, 1.0 / zoom, offset); - } + public void initialize(Transaction tx) { + Envelope bbox = index.getBoundingBox(tx); + if (bbox != null) { + bounds.expandToInclude(Utilities.fromNeo4jToJts(bbox)); + } + bounds = SpatialTopologyUtils.adjustBounds(bounds, 1.0 / zoom, offset); + } - public void saveRTreeLayers(Transaction tx, File imagefile, int levels) throws IOException { - saveRTreeLayers(tx, imagefile, levels, new EmptyMonitor(), new ArrayList<>(), null, null); - } + public void initialize(Transaction tx, Coordinate min, Coordinate max) { + Envelope bbox = index.getBoundingBox(tx); + if (bbox != null) { + bounds.expandToInclude(Utilities.fromNeo4jToJts(bbox)); + } + bounds.expandToInclude(new org.locationtech.jts.geom.Envelope(min.x, max.x, min.y, max.y)); + bounds = SpatialTopologyUtils.adjustBounds(bounds, 1.0 / zoom, offset); + } - public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, int levels) throws IOException { - saveRTreeLayers(tx, imagefile, rootNode, levels, new EmptyMonitor(), new ArrayList<>(), new ArrayList<>(), null, null); - } + public void saveRTreeLayers(Transaction tx, File imagefile, int levels) throws IOException { + saveRTreeLayers(tx, imagefile, levels, new EmptyMonitor(), new ArrayList<>(), null, null); + } - public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, List envelopes, int levels) throws IOException { - saveRTreeLayers(tx, imagefile, rootNode, levels, new EmptyMonitor(), new ArrayList<>(), envelopes, null, null); - } + public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, int levels) throws IOException { + saveRTreeLayers(tx, imagefile, rootNode, levels, new EmptyMonitor(), new ArrayList<>(), new ArrayList<>(), null, + null); + } - public void saveRTreeLayers(Transaction tx, File imagefile, int levels, TreeMonitor monitor) throws IOException { - saveRTreeLayers(tx, imagefile, levels, monitor, new ArrayList<>(), null, null); - } + public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, List envelopes, int levels) + throws IOException { + saveRTreeLayers(tx, imagefile, rootNode, levels, new EmptyMonitor(), new ArrayList<>(), envelopes, null, null); + } - public void saveRTreeLayers(Transaction tx, File imagefile, int levels, TreeMonitor monitor, List foundNodes, Coordinate min, Coordinate max) throws IOException { - saveRTreeLayers(tx, imagefile, index.getIndexRoot(tx), levels, monitor, foundNodes, new ArrayList<>(), min, max); - } + public void saveRTreeLayers(Transaction tx, File imagefile, int levels, TreeMonitor monitor) throws IOException { + saveRTreeLayers(tx, imagefile, levels, monitor, new ArrayList<>(), null, null); + } - public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, int levels, TreeMonitor monitor, List foundNodes, List envelopes, Coordinate min, Coordinate max) throws IOException { - MapContent mapContent = new MapContent(); - drawBounds(mapContent, bounds, Color.WHITE); + public void saveRTreeLayers(Transaction tx, File imagefile, int levels, TreeMonitor monitor, List foundNodes, + Coordinate min, Coordinate max) throws IOException { + saveRTreeLayers(tx, imagefile, index.getIndexRoot(tx), levels, monitor, foundNodes, new ArrayList<>(), min, + max); + } - int indexHeight = index.getHeight(rootNode, 0); - ArrayList> layers = new ArrayList<>(indexHeight); - ArrayList> indexMatches = new ArrayList<>(indexHeight); - for (int i = 0; i < indexHeight; i++) { - indexMatches.add(monitor.getMatchedTreeNodes(indexHeight - i - 1).stream() - .map(n -> new RTreeIndex.NodeWithEnvelope(n, index.getLeafNodeEnvelope(n))) - .collect(Collectors.toList())); - layers.add(new ArrayList<>()); - ArrayList nodes = layers.get(i); - if (i == 0) { - nodes.add(new RTreeIndex.NodeWithEnvelope(rootNode, index.getIndexNodeEnvelope(rootNode))); - } else { - for (RTreeIndex.NodeWithEnvelope parent : layers.get(i - 1)) { - for (RTreeIndex.NodeWithEnvelope child : index.getIndexChildren(parent.node)) { - layers.get(i).add(child); - } - } - } - } - ArrayList allIndexedNodes = new ArrayList<>(); - for (Node node : index.getAllIndexedNodes(tx)) { - allIndexedNodes.add(node); - } - drawGeometryNodes(mapContent, allIndexedNodes, Color.LIGHT_GRAY); - for (int level = 0; level < Math.min(indexHeight, levels); level++) { - ArrayList layer = layers.get(indexHeight - level - 1); - System.out.println("Drawing index level " + level + " of " + layer.size() + " nodes"); - drawIndexNodes(level, mapContent, layer, colors[level % colors.length]); - drawIndexNodes(2 + level * 2, mapContent, indexMatches.get(level), Color.MAGENTA); - } - drawEnvelopes(mapContent, envelopes, Color.ORANGE); - drawGeometryNodes(mapContent, foundNodes, Color.RED); - if (min != null && max != null) { - drawEnvelope(mapContent, min, max, Color.RED); - } - saveMapContentToImageFile(mapContent, imagefile, bounds); - } + public void saveRTreeLayers(Transaction tx, File imagefile, Node rootNode, int levels, TreeMonitor monitor, + List foundNodes, List envelopes, Coordinate min, Coordinate max) throws IOException { + MapContent mapContent = new MapContent(); + drawBounds(mapContent, bounds, Color.WHITE); - private void saveMapContentToImageFile(MapContent mapContent, File imagefile, ReferencedEnvelope bounds) throws IOException { - RenderingHints hints = new RenderingHints(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); - hints.put(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON); + int indexHeight = index.getHeight(rootNode, 0); + ArrayList> layers = new ArrayList<>(indexHeight); + ArrayList> indexMatches = new ArrayList<>(indexHeight); + for (int i = 0; i < indexHeight; i++) { + indexMatches.add(monitor.getMatchedTreeNodes(indexHeight - i - 1).stream() + .map(n -> new RTreeIndex.NodeWithEnvelope(n, index.getLeafNodeEnvelope(n))) + .collect(Collectors.toList())); + layers.add(new ArrayList<>()); + ArrayList nodes = layers.get(i); + if (i == 0) { + nodes.add(new RTreeIndex.NodeWithEnvelope(rootNode, index.getIndexNodeEnvelope(rootNode))); + } else { + for (RTreeIndex.NodeWithEnvelope parent : layers.get(i - 1)) { + for (RTreeIndex.NodeWithEnvelope child : index.getIndexChildren(parent.node)) { + layers.get(i).add(child); + } + } + } + } + ArrayList allIndexedNodes = new ArrayList<>(); + for (Node node : index.getAllIndexedNodes(tx)) { + allIndexedNodes.add(node); + } + drawGeometryNodes(mapContent, allIndexedNodes, Color.LIGHT_GRAY); + for (int level = 0; level < Math.min(indexHeight, levels); level++) { + ArrayList layer = layers.get(indexHeight - level - 1); + System.out.println("Drawing index level " + level + " of " + layer.size() + " nodes"); + drawIndexNodes(level, mapContent, layer, colors[level % colors.length]); + drawIndexNodes(2 + level * 2, mapContent, indexMatches.get(level), Color.MAGENTA); + } + drawEnvelopes(mapContent, envelopes, Color.ORANGE); + drawGeometryNodes(mapContent, foundNodes, Color.RED); + if (min != null && max != null) { + drawEnvelope(mapContent, min, max, Color.RED); + } + saveMapContentToImageFile(mapContent, imagefile, bounds); + } - BufferedImage image = new BufferedImage(displaySize.width, displaySize.height, BufferedImage.TYPE_INT_ARGB); - Graphics2D graphics = image.createGraphics(); + private void saveMapContentToImageFile(MapContent mapContent, File imagefile, ReferencedEnvelope bounds) + throws IOException { + RenderingHints hints = new RenderingHints(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + hints.put(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON); - StreamingRenderer renderer = new StreamingRenderer(); - renderer.setJava2DHints(hints); - renderer.setMapContent(mapContent); - renderer.paint(graphics, displaySize, bounds); + BufferedImage image = new BufferedImage(displaySize.width, displaySize.height, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); - graphics.dispose(); - mapContent.dispose(); + StreamingRenderer renderer = new StreamingRenderer(); + renderer.setJava2DHints(hints); + renderer.setMapContent(mapContent); + renderer.paint(graphics, displaySize, bounds); - imagefile = checkFile(imagefile); - System.out.println("Writing image to disk: " + imagefile); - ImageIO.write(image, "png", imagefile); - } + graphics.dispose(); + mapContent.dispose(); - private File checkFile(File file) { - if (!file.isAbsolute() && exportDir != null) { - file = new File(exportDir, file.getPath()); - } - file = file.getAbsoluteFile(); - file.getParentFile().mkdirs(); - if (file.exists()) { - System.out.println("Deleting previous index image file: " + file); - file.delete(); - } - return file; - } + imagefile = checkFile(imagefile); + System.out.println("Writing image to disk: " + imagefile); + ImageIO.write(image, "png", imagefile); + } - private void drawGeometryNodes(MapContent mapContent, List nodes, Color color) { - Style style = StyledImageExporter.createStyleFromGeometry(featureType, color, Color.GRAY); - mapContent.addLayer(new org.geotools.map.FeatureLayer(makeGeometryNodeFeatures(nodes, featureType), style)); - } + private File checkFile(File file) { + if (!file.isAbsolute() && exportDir != null) { + file = new File(exportDir, file.getPath()); + } + file = file.getAbsoluteFile(); + file.getParentFile().mkdirs(); + if (file.exists()) { + System.out.println("Deleting previous index image file: " + file); + file.delete(); + } + return file; + } - private void drawEnvelopes(MapContent mapContent, List envelopes, Color color) { - Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, 3); - mapContent.addLayer(new org.geotools.map.FeatureLayer(makeEnvelopeFeatures(envelopes), style)); - } + private void drawGeometryNodes(MapContent mapContent, List nodes, Color color) { + Style style = StyledImageExporter.createStyleFromGeometry(featureType, color, Color.GRAY); + mapContent.addLayer(new org.geotools.map.FeatureLayer(makeGeometryNodeFeatures(nodes, featureType), style)); + } - private void drawIndexNodes(int level, MapContent mapContent, List nodes, Color color) throws IOException { - Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, level + 1); - mapContent.addLayer(new org.geotools.map.FeatureLayer(makeIndexNodeFeatures(nodes), style)); - } + private void drawEnvelopes(MapContent mapContent, List envelopes, Color color) { + Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, 3); + mapContent.addLayer(new org.geotools.map.FeatureLayer(makeEnvelopeFeatures(envelopes), style)); + } - private void drawEnvelope(MapContent mapContent, Coordinate min, Coordinate max, Color color) throws IOException { - Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, 3); - mapContent.addLayer(new org.geotools.map.FeatureLayer(makeEnvelopeFeatures(min, max), style)); - } + private void drawIndexNodes(int level, MapContent mapContent, List nodes, Color color) + throws IOException { + Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, level + 1); + mapContent.addLayer(new org.geotools.map.FeatureLayer(makeIndexNodeFeatures(nodes), style)); + } - private void drawBounds(MapContent mapContent, ReferencedEnvelope bounds, Color color) throws IOException { - Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 1.0, 6); - double[] min = bounds.getLowerCorner().getCoordinate(); - double[] max = bounds.getUpperCorner().getCoordinate(); - mapContent.addLayer(new org.geotools.map.FeatureLayer(makeEnvelopeFeatures(new Coordinate(min[0], min[1]), new Coordinate(max[0], max[1])), style)); - } + private void drawEnvelope(MapContent mapContent, Coordinate min, Coordinate max, Color color) throws IOException { + Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 0.0, 3); + mapContent.addLayer(new org.geotools.map.FeatureLayer(makeEnvelopeFeatures(min, max), style)); + } - private MemoryFeatureCollection makeEnvelopeFeatures(List envelopes) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, new String[]{}); - Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); - MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); - for (Envelope envelope : envelopes) { + private void drawBounds(MapContent mapContent, ReferencedEnvelope bounds, Color color) throws IOException { + Style style = StyledImageExporter.createPolygonStyle(color, Color.WHITE, 0.8, 1.0, 6); + double[] min = bounds.getLowerCorner().getCoordinate(); + double[] max = bounds.getUpperCorner().getCoordinate(); + mapContent.addLayer(new org.geotools.map.FeatureLayer( + makeEnvelopeFeatures(new Coordinate(min[0], min[1]), new Coordinate(max[0], max[1])), style)); + } + private MemoryFeatureCollection makeEnvelopeFeatures(List envelopes) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, + new String[]{}); + Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); + MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); + for (Envelope envelope : envelopes) { - Coordinate[] coordinates = new Coordinate[]{ - new Coordinate(envelope.getMinX(), envelope.getMinY()), - new Coordinate(envelope.getMinX(), envelope.getMaxY()), - new Coordinate(envelope.getMaxX(), envelope.getMaxY()), - new Coordinate(envelope.getMaxX(), envelope.getMinY()), - new Coordinate(envelope.getMinX(), envelope.getMinY()) - }; - Geometry geometry = geometryFactory.createPolygon(coordinates); - features.add(featureBuilder.buildFeature("envelope", geometry, new HashMap<>())); - } - return features; - } + Coordinate[] coordinates = new Coordinate[]{ + new Coordinate(envelope.getMinX(), envelope.getMinY()), + new Coordinate(envelope.getMinX(), envelope.getMaxY()), + new Coordinate(envelope.getMaxX(), envelope.getMaxY()), + new Coordinate(envelope.getMaxX(), envelope.getMinY()), + new Coordinate(envelope.getMinX(), envelope.getMinY()) + }; + Geometry geometry = geometryFactory.createPolygon(coordinates); + features.add(featureBuilder.buildFeature("envelope", geometry, new HashMap<>())); + } + return features; + } - private MemoryFeatureCollection makeEnvelopeFeatures(Coordinate min, Coordinate max) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, new String[]{}); - Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); - MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); - Coordinate[] coordinates = new Coordinate[]{ - new Coordinate(min.x, min.y), - new Coordinate(min.x, max.y), - new Coordinate(max.x, max.y), - new Coordinate(max.x, min.y), - new Coordinate(min.x, min.y) - }; - Geometry geometry = geometryFactory.createPolygon(coordinates); - features.add(featureBuilder.buildFeature("envelope", geometry, new HashMap<>())); - return features; - } + private MemoryFeatureCollection makeEnvelopeFeatures(Coordinate min, Coordinate max) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, + new String[]{}); + Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); + MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); + Coordinate[] coordinates = new Coordinate[]{ + new Coordinate(min.x, min.y), + new Coordinate(min.x, max.y), + new Coordinate(max.x, max.y), + new Coordinate(max.x, min.y), + new Coordinate(min.x, min.y) + }; + Geometry geometry = geometryFactory.createPolygon(coordinates); + features.add(featureBuilder.buildFeature("envelope", geometry, new HashMap<>())); + return features; + } - private MemoryFeatureCollection makeIndexNodeFeatures(List nodes) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, new String[]{}); - Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); - MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); - for (int i = 0; i < nodes.size(); i++) { - RTreeIndex.NodeWithEnvelope node = nodes.get(i); - Envelope envelope = node.envelope; - Coordinate[] coordinates = new Coordinate[]{ - new Coordinate(envelope.getMinX(), envelope.getMinY()), - new Coordinate(envelope.getMinX(), envelope.getMaxY()), - new Coordinate(envelope.getMaxX(), envelope.getMaxY()), - new Coordinate(envelope.getMaxX(), envelope.getMinY()), - new Coordinate(envelope.getMinX(), envelope.getMinY()) - }; - Geometry geometry = geometryFactory.createPolygon(coordinates); - features.add(featureBuilder.buildFeature(node.toString(), geometry, new HashMap<>())); - } - return features; - } + private MemoryFeatureCollection makeIndexNodeFeatures(List nodes) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("Polygon", Constants.GTYPE_POLYGON, crs, + new String[]{}); + Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); + MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); + for (int i = 0; i < nodes.size(); i++) { + RTreeIndex.NodeWithEnvelope node = nodes.get(i); + Envelope envelope = node.envelope; + Coordinate[] coordinates = new Coordinate[]{ + new Coordinate(envelope.getMinX(), envelope.getMinY()), + new Coordinate(envelope.getMinX(), envelope.getMaxY()), + new Coordinate(envelope.getMaxX(), envelope.getMaxY()), + new Coordinate(envelope.getMaxX(), envelope.getMinY()), + new Coordinate(envelope.getMinX(), envelope.getMinY()) + }; + Geometry geometry = geometryFactory.createPolygon(coordinates); + features.add(featureBuilder.buildFeature(node.toString(), geometry, new HashMap<>())); + } + return features; + } - private MemoryFeatureCollection makeGeometryNodeFeatures(List nodes, SimpleFeatureType featureType) { - Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); - MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); - for (Node node : nodes) { - Geometry geometry = geometryEncoder.decodeGeometry(node); - features.add(featureBuilder.buildFeature(node.toString(), geometry, new HashMap<>())); - } - return features; - } + private MemoryFeatureCollection makeGeometryNodeFeatures(List nodes, SimpleFeatureType featureType) { + Neo4jFeatureBuilder featureBuilder = new Neo4jFeatureBuilder(featureType, new ArrayList()); + MemoryFeatureCollection features = new MemoryFeatureCollection(featureType); + for (Node node : nodes) { + Geometry geometry = geometryEncoder.decodeGeometry(node); + features.add(featureBuilder.buildFeature(node.toString(), geometry, new HashMap<>())); + } + return features; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeIndex.java b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeIndex.java index b68729159..6dce0c700 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeIndex.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeIndex.java @@ -55,1448 +55,1492 @@ */ public class RTreeIndex implements SpatialIndexWriter, Configurable { - public static final String INDEX_PROP_BBOX = "bbox"; - - public static final String KEY_SPLIT = "splitMode"; - public static final String QUADRATIC_SPLIT = "quadratic"; - public static final String GREENES_SPLIT = "greene"; - - public static final String KEY_MAX_NODE_REFERENCES = "maxNodeReferences"; - public static final String KEY_SHOULD_MERGE_TREES = "shouldMergeTrees"; - public static final int MIN_MAX_NODE_REFERENCES = 10; - public static final int MAX_MAX_NODE_REFERENCES = 1000000; - public static final int DEFAULT_MAX_NODE_REFERENCES = 100; - - private TreeMonitor monitor; - private String rootNodeId; - private EnvelopeDecoder envelopeDecoder; - private int maxNodeReferences; - private String splitMode = GREENES_SPLIT; - private boolean shouldMergeTrees = false; - - private int totalGeometryCount = 0; - private boolean countSaved = false; - - public void addMonitor(TreeMonitor monitor) { - this.monitor = monitor; - } - - public void init(Transaction tx, Node layerNode, EnvelopeDecoder envelopeDecoder, int maxNodeReferences) { - this.rootNodeId = layerNode.getElementId(); - this.envelopeDecoder = envelopeDecoder; - this.maxNodeReferences = maxNodeReferences; - monitor = new EmptyMonitor(); - if (envelopeDecoder == null) { - throw new NullPointerException("envelopeDecoder is NULL"); - } - initIndexRoot(tx); - initIndexMetadata(tx); - } - - // Public methods - @Override - public EnvelopeDecoder getEnvelopeDecoder() { - return this.envelopeDecoder; - } - - @Override - public void setConfiguration(String jsonConfig) { - JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonConfig); - HashMap config = new HashMap<>(); - for (Object key : jsonObject.keySet()) { - config.put(key.toString(), jsonObject.get(key)); - } - configure(config); - } - - public String getConfiguration() { - HashMap config = new HashMap<>(); - config.put(KEY_SPLIT, this.splitMode); - config.put(KEY_MAX_NODE_REFERENCES, this.maxNodeReferences); - config.put(KEY_SHOULD_MERGE_TREES, this.shouldMergeTrees); - return JSONObject.toJSONString(config); - } - - public void configure(Map config) { - for (String key : config.keySet()) { - switch (key) { - case KEY_SPLIT: - String value = config.get(key).toString(); - switch (value) { - case QUADRATIC_SPLIT: - case GREENES_SPLIT: - splitMode = value; - break; - default: - throw new IllegalArgumentException("No such RTreeIndex value for '" + key + "': " + value); - } - break; - case KEY_MAX_NODE_REFERENCES: - int intValue = Integer.parseInt(config.get(key).toString()); - if (intValue < MIN_MAX_NODE_REFERENCES) { - throw new IllegalArgumentException("RTreeIndex does not allow " + key + " less than " + MIN_MAX_NODE_REFERENCES); - } - if (intValue > MAX_MAX_NODE_REFERENCES) { - throw new IllegalArgumentException("RTreeIndex does not allow " + key + " greater than " + MAX_MAX_NODE_REFERENCES); - } - this.maxNodeReferences = intValue; - break; - case KEY_SHOULD_MERGE_TREES: - this.shouldMergeTrees = Boolean.parseBoolean(config.get(key).toString()); - break; - default: - throw new IllegalArgumentException("No such RTreeIndex configuration key: " + key); - } - } - } - - @Override - public void add(Transaction tx, Node geomNode) { - // initialize the search with root - Node parent = getIndexRoot(tx); - addBelow(tx, parent, geomNode); - countSaved = false; - totalGeometryCount++; - } - - /** - * This method will add the node somewhere below the parent. - */ - private void addBelow(Transaction tx, Node parent, Node geomNode) { - // choose a path down to a leaf - while (!nodeIsLeaf(parent)) { - parent = chooseSubTree(parent, geomNode); - } - if (countChildren(parent, RTreeRelationshipTypes.RTREE_REFERENCE) >= maxNodeReferences) { - insertInLeaf(parent, geomNode); - splitAndAdjustPathBoundingBox(tx, parent); - } else { - if (insertInLeaf(parent, geomNode)) { - // bbox enlargement needed - adjustPathBoundingBox(parent); - } - } - } - - - /** - * Use this method if you want to insert an index node as a child of a given index node. This will recursively - * update the bounding boxes above the parent to keep the tree consistent. - */ - private void insertIndexNodeOnParent(Transaction tx, Node parent, Node child) { - int numChildren = countChildren(parent, RTreeRelationshipTypes.RTREE_CHILD); - boolean needExpansion = addChild(parent, RTreeRelationshipTypes.RTREE_CHILD, child); - if (numChildren < maxNodeReferences) { - if (needExpansion) { - adjustPathBoundingBox(parent); - } - } else { - splitAndAdjustPathBoundingBox(tx, parent); - } - } - - - /** - * Depending on the size of the incumbent tree, this will either attempt to rebuild the entire index from scratch - * (strategy used if the insert larger than 40% of the current tree size - may give heap out of memory errors for - * large inserts as has O(n) space complexity in the total tree size. It has n*log(n) time complexity. See function - * partition for more details.) or it will insert using the method of seeded clustering, where you attempt to use the - * existing tree structure to partition your data. - *

- * This is based on the Paper "Bulk Insertion for R-trees by seeded clustering" by T.Lee, S.Lee & B Moon. - * Repeated use of this strategy will lead to degraded query performance, especially if used for - * many relatively small insertions compared to tree size. Though not worse than one by one insertion. - * In practice, it should be fine for most uses. - */ - @Override - public void add(Transaction tx, List geomNodes) { - - //If the insertion is large relative to the size of the tree, simply rebuild the whole tree. - if (geomNodes.size() > totalGeometryCount * 0.4) { - List nodesToAdd = new ArrayList<>(geomNodes.size() + totalGeometryCount); - for (Node n : getAllIndexedNodes(tx)) { - nodesToAdd.add(n); - } - nodesToAdd.addAll(geomNodes); - detachGeometryNodes(tx, false, getIndexRoot(tx), new NullListener()); - deleteTreeBelow(tx, getIndexRoot(tx)); - buildRtreeFromScratch(tx, getIndexRoot(tx), decodeGeometryNodeEnvelopes(nodesToAdd), 0.7); - countSaved = false; - totalGeometryCount = nodesToAdd.size(); - monitor.addNbrRebuilt(this, tx); - } else { - - List outliers = bulkInsertion(tx, getIndexRoot(tx), getHeight(getIndexRoot(tx), 0), decodeGeometryNodeEnvelopes(geomNodes), 0.7); - countSaved = false; - totalGeometryCount = totalGeometryCount + (geomNodes.size() - outliers.size()); - for (NodeWithEnvelope n : outliers) { - add(tx, n.node); - } - } - } - - private List decodeGeometryNodeEnvelopes(List nodes) { - return nodes.stream().map(GeometryNodeWithEnvelope::new).collect(Collectors.toList()); - } - - public static class NodeWithEnvelope { - public Envelope envelope; - Node node; - - public NodeWithEnvelope(Node node, Envelope envelope) { - this.node = node; - this.envelope = envelope; - } - - /** - * Ensure this node is valid in the specified transaction - */ - public NodeWithEnvelope refresh(Transaction tx) { - this.node = tx.getNodeByElementId(this.node.getElementId()); - return this; - } - } - - public class GeometryNodeWithEnvelope extends NodeWithEnvelope { - GeometryNodeWithEnvelope(Node node) { - super(node, envelopeDecoder.decodeEnvelope(node)); - } - } - - /** - * Returns the height of the tree, starting with the rootNode and adding one for each subsequent level. Relies on the - * balanced property of the RTree that all leaves are on the same level and no index nodes are empty. In the convention - * the index is level 0, so if there is just the index and the leaf nodes, the leaf nodes are level one and the height is one. - * Thus the lowest level is 1. - */ - int getHeight(Node rootNode, int height) { - try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - if (relationships.iterator().hasNext()) { - return getHeight(relationships.iterator().next().getEndNode(), height + 1); - } else { - // Add one to account for the step to leaf nodes. - return height + 1; // todo should this really be +1 ? - } - } - } - - List getIndexChildren(Node rootNode) { - List result = new ArrayList<>(); - try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship r : relationships) { - Node child = r.getEndNode(); - result.add(new NodeWithEnvelope(child, getIndexNodeEnvelope(child))); - } - } - return result; - } - - private List getIndexChildren(Node rootNode, int depth) { - if (depth < 1) { - throw new IllegalArgumentException("Depths must be at least one"); - } - - List rootChildren = getIndexChildren(rootNode); - if (depth == 1) { - return rootChildren; - } else { - List result = new ArrayList<>(rootChildren.size() * 5); - for (NodeWithEnvelope child : rootChildren) { - result.addAll(getIndexChildren(child.node, depth - 1)); - } - return result; - } - } - - private List bulkInsertion(Transaction tx, Node rootNode, int rootNodeHeight, final List geomNodes, final double loadingFactor) { - List children = getIndexChildren(rootNode); - if (children.isEmpty()) { - return geomNodes; - } - children.sort(new IndexNodeAreaComparator()); - - Map> map = new HashMap<>(children.size()); - int nodesPerRootSubTree = Math.max(16, geomNodes.size() / children.size()); - for (NodeWithEnvelope n : children) { - map.put(n, new ArrayList<>(nodesPerRootSubTree)); - } - - // The outliers are those nodes which do not fit into the existing tree hierarchy. - List outliers = new ArrayList<>(geomNodes.size() / 10); // 10% outliers - for (NodeWithEnvelope n : geomNodes) { - Envelope env = n.envelope; - boolean flag = true; - - //exploits that the iterator returns the list inorder, which is sorted by size, as above. Thus child - //is always added to the smallest existing envelope which contains it. - for (NodeWithEnvelope c : children) { - if (c.envelope.contains(env)) { - map.get(c).add(n); //add to smallest area envelope which contains the child; - flag = false; - break; - } - } - // else add to outliers. - if (flag) { - outliers.add(n); - } - } - for (NodeWithEnvelope child : children) { - List cluster = map.get(child); - - if (cluster.isEmpty()) continue; - - // todo move each branch into a named method - int expectedHeight = expectedHeight(loadingFactor, cluster.size()); - - //In an rtree is this height it will add as a single child to the current child node. - int currentRTreeHeight = rootNodeHeight - 2; + public static final String INDEX_PROP_BBOX = "bbox"; + + public static final String KEY_SPLIT = "splitMode"; + public static final String QUADRATIC_SPLIT = "quadratic"; + public static final String GREENES_SPLIT = "greene"; + + public static final String KEY_MAX_NODE_REFERENCES = "maxNodeReferences"; + public static final String KEY_SHOULD_MERGE_TREES = "shouldMergeTrees"; + public static final int MIN_MAX_NODE_REFERENCES = 10; + public static final int MAX_MAX_NODE_REFERENCES = 1000000; + public static final int DEFAULT_MAX_NODE_REFERENCES = 100; + + private TreeMonitor monitor; + private String rootNodeId; + private EnvelopeDecoder envelopeDecoder; + private int maxNodeReferences; + private String splitMode = GREENES_SPLIT; + private boolean shouldMergeTrees = false; + + private int totalGeometryCount = 0; + private boolean countSaved = false; + + public void addMonitor(TreeMonitor monitor) { + this.monitor = monitor; + } + + public void init(Transaction tx, Node layerNode, EnvelopeDecoder envelopeDecoder, int maxNodeReferences) { + this.rootNodeId = layerNode.getElementId(); + this.envelopeDecoder = envelopeDecoder; + this.maxNodeReferences = maxNodeReferences; + monitor = new EmptyMonitor(); + if (envelopeDecoder == null) { + throw new NullPointerException("envelopeDecoder is NULL"); + } + initIndexRoot(tx); + initIndexMetadata(tx); + } + + // Public methods + @Override + public EnvelopeDecoder getEnvelopeDecoder() { + return this.envelopeDecoder; + } + + @Override + public void setConfiguration(String jsonConfig) { + JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonConfig); + HashMap config = new HashMap<>(); + for (Object key : jsonObject.keySet()) { + config.put(key.toString(), jsonObject.get(key)); + } + configure(config); + } + + public String getConfiguration() { + HashMap config = new HashMap<>(); + config.put(KEY_SPLIT, this.splitMode); + config.put(KEY_MAX_NODE_REFERENCES, this.maxNodeReferences); + config.put(KEY_SHOULD_MERGE_TREES, this.shouldMergeTrees); + return JSONObject.toJSONString(config); + } + + public void configure(Map config) { + for (String key : config.keySet()) { + switch (key) { + case KEY_SPLIT: + String value = config.get(key).toString(); + switch (value) { + case QUADRATIC_SPLIT: + case GREENES_SPLIT: + splitMode = value; + break; + default: + throw new IllegalArgumentException("No such RTreeIndex value for '" + key + "': " + value); + } + break; + case KEY_MAX_NODE_REFERENCES: + int intValue = Integer.parseInt(config.get(key).toString()); + if (intValue < MIN_MAX_NODE_REFERENCES) { + throw new IllegalArgumentException( + "RTreeIndex does not allow " + key + " less than " + MIN_MAX_NODE_REFERENCES); + } + if (intValue > MAX_MAX_NODE_REFERENCES) { + throw new IllegalArgumentException( + "RTreeIndex does not allow " + key + " greater than " + MAX_MAX_NODE_REFERENCES); + } + this.maxNodeReferences = intValue; + break; + case KEY_SHOULD_MERGE_TREES: + this.shouldMergeTrees = Boolean.parseBoolean(config.get(key).toString()); + break; + default: + throw new IllegalArgumentException("No such RTreeIndex configuration key: " + key); + } + } + } + + @Override + public void add(Transaction tx, Node geomNode) { + // initialize the search with root + Node parent = getIndexRoot(tx); + addBelow(tx, parent, geomNode); + countSaved = false; + totalGeometryCount++; + } + + /** + * This method will add the node somewhere below the parent. + */ + private void addBelow(Transaction tx, Node parent, Node geomNode) { + // choose a path down to a leaf + while (!nodeIsLeaf(parent)) { + parent = chooseSubTree(parent, geomNode); + } + if (countChildren(parent, RTreeRelationshipTypes.RTREE_REFERENCE) >= maxNodeReferences) { + insertInLeaf(parent, geomNode); + splitAndAdjustPathBoundingBox(tx, parent); + } else { + if (insertInLeaf(parent, geomNode)) { + // bbox enlargement needed + adjustPathBoundingBox(parent); + } + } + } + + + /** + * Use this method if you want to insert an index node as a child of a given index node. This will recursively + * update the bounding boxes above the parent to keep the tree consistent. + */ + private void insertIndexNodeOnParent(Transaction tx, Node parent, Node child) { + int numChildren = countChildren(parent, RTreeRelationshipTypes.RTREE_CHILD); + boolean needExpansion = addChild(parent, RTreeRelationshipTypes.RTREE_CHILD, child); + if (numChildren < maxNodeReferences) { + if (needExpansion) { + adjustPathBoundingBox(parent); + } + } else { + splitAndAdjustPathBoundingBox(tx, parent); + } + } + + + /** + * Depending on the size of the incumbent tree, this will either attempt to rebuild the entire index from scratch + * (strategy used if the insert larger than 40% of the current tree size - may give heap out of memory errors for + * large inserts as has O(n) space complexity in the total tree size. It has n*log(n) time complexity. See function + * partition for more details.) or it will insert using the method of seeded clustering, where you attempt to use + * the + * existing tree structure to partition your data. + *

+ * This is based on the Paper "Bulk Insertion for R-trees by seeded clustering" by T.Lee, S.Lee & B Moon. + * Repeated use of this strategy will lead to degraded query performance, especially if used for + * many relatively small insertions compared to tree size. Though not worse than one by one insertion. + * In practice, it should be fine for most uses. + */ + @Override + public void add(Transaction tx, List geomNodes) { + + //If the insertion is large relative to the size of the tree, simply rebuild the whole tree. + if (geomNodes.size() > totalGeometryCount * 0.4) { + List nodesToAdd = new ArrayList<>(geomNodes.size() + totalGeometryCount); + for (Node n : getAllIndexedNodes(tx)) { + nodesToAdd.add(n); + } + nodesToAdd.addAll(geomNodes); + detachGeometryNodes(tx, false, getIndexRoot(tx), new NullListener()); + deleteTreeBelow(tx, getIndexRoot(tx)); + buildRtreeFromScratch(tx, getIndexRoot(tx), decodeGeometryNodeEnvelopes(nodesToAdd), 0.7); + countSaved = false; + totalGeometryCount = nodesToAdd.size(); + monitor.addNbrRebuilt(this, tx); + } else { + + List outliers = bulkInsertion(tx, getIndexRoot(tx), getHeight(getIndexRoot(tx), 0), + decodeGeometryNodeEnvelopes(geomNodes), 0.7); + countSaved = false; + totalGeometryCount = totalGeometryCount + (geomNodes.size() - outliers.size()); + for (NodeWithEnvelope n : outliers) { + add(tx, n.node); + } + } + } + + private List decodeGeometryNodeEnvelopes(List nodes) { + return nodes.stream().map(GeometryNodeWithEnvelope::new).collect(Collectors.toList()); + } + + public static class NodeWithEnvelope { + + public Envelope envelope; + Node node; + + public NodeWithEnvelope(Node node, Envelope envelope) { + this.node = node; + this.envelope = envelope; + } + + /** + * Ensure this node is valid in the specified transaction + */ + public NodeWithEnvelope refresh(Transaction tx) { + this.node = tx.getNodeByElementId(this.node.getElementId()); + return this; + } + } + + public class GeometryNodeWithEnvelope extends NodeWithEnvelope { + + GeometryNodeWithEnvelope(Node node) { + super(node, envelopeDecoder.decodeEnvelope(node)); + } + } + + /** + * Returns the height of the tree, starting with the rootNode and adding one for each subsequent level. Relies on + * the + * balanced property of the RTree that all leaves are on the same level and no index nodes are empty. In the + * convention + * the index is level 0, so if there is just the index and the leaf nodes, the leaf nodes are level one and the + * height is one. + * Thus the lowest level is 1. + */ + int getHeight(Node rootNode, int height) { + try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + if (relationships.iterator().hasNext()) { + return getHeight(relationships.iterator().next().getEndNode(), height + 1); + } else { + // Add one to account for the step to leaf nodes. + return height + 1; // todo should this really be +1 ? + } + } + } + + List getIndexChildren(Node rootNode) { + List result = new ArrayList<>(); + try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship r : relationships) { + Node child = r.getEndNode(); + result.add(new NodeWithEnvelope(child, getIndexNodeEnvelope(child))); + } + } + return result; + } + + private List getIndexChildren(Node rootNode, int depth) { + if (depth < 1) { + throw new IllegalArgumentException("Depths must be at least one"); + } + + List rootChildren = getIndexChildren(rootNode); + if (depth == 1) { + return rootChildren; + } else { + List result = new ArrayList<>(rootChildren.size() * 5); + for (NodeWithEnvelope child : rootChildren) { + result.addAll(getIndexChildren(child.node, depth - 1)); + } + return result; + } + } + + private List bulkInsertion(Transaction tx, Node rootNode, int rootNodeHeight, + final List geomNodes, final double loadingFactor) { + List children = getIndexChildren(rootNode); + if (children.isEmpty()) { + return geomNodes; + } + children.sort(new IndexNodeAreaComparator()); + + Map> map = new HashMap<>(children.size()); + int nodesPerRootSubTree = Math.max(16, geomNodes.size() / children.size()); + for (NodeWithEnvelope n : children) { + map.put(n, new ArrayList<>(nodesPerRootSubTree)); + } + + // The outliers are those nodes which do not fit into the existing tree hierarchy. + List outliers = new ArrayList<>(geomNodes.size() / 10); // 10% outliers + for (NodeWithEnvelope n : geomNodes) { + Envelope env = n.envelope; + boolean flag = true; + + //exploits that the iterator returns the list inorder, which is sorted by size, as above. Thus child + //is always added to the smallest existing envelope which contains it. + for (NodeWithEnvelope c : children) { + if (c.envelope.contains(env)) { + map.get(c).add(n); //add to smallest area envelope which contains the child; + flag = false; + break; + } + } + // else add to outliers. + if (flag) { + outliers.add(n); + } + } + for (NodeWithEnvelope child : children) { + List cluster = map.get(child); + + if (cluster.isEmpty()) { + continue; + } + + // todo move each branch into a named method + int expectedHeight = expectedHeight(loadingFactor, cluster.size()); + + //In an rtree is this height it will add as a single child to the current child node. + int currentRTreeHeight = rootNodeHeight - 2; // if(expectedHeight-currentRTreeHeight > 1 ){ // throw new RuntimeException("Due to h_i-l_t > 1"); // } - if (expectedHeight < currentRTreeHeight) { - monitor.addCase("h_i < l_t "); - //if the height is smaller than that recursively sort and split. - outliers.addAll(bulkInsertion(tx, child.node, rootNodeHeight - 1, cluster, loadingFactor)); - } //if constructed tree is the correct size insert it here. - else if (expectedHeight == currentRTreeHeight) { - - //Do not create underfull nodes, instead use the add logic, except we know the root not to add them too. - //this handles the case where the number of nodes in a cluster is small. - - if (cluster.size() < maxNodeReferences * loadingFactor / 2) { - monitor.addCase("h_i == l_t && small cluster"); - // getParent because addition might cause a split. This strategy not ideal, - // but does tend to limit overlap more than adding to the child exclusively. - - for (NodeWithEnvelope n : cluster) { - addBelow(tx, rootNode, n.node); - } - } else { - monitor.addCase("h_i == l_t && big cluster"); - Node newRootNode = tx.createNode(); - buildRtreeFromScratch(tx, newRootNode, cluster, loadingFactor); - if (shouldMergeTrees) { - NodeWithEnvelope nodeWithEnvelope = new NodeWithEnvelope(newRootNode, getIndexNodeEnvelope(newRootNode)); - List insert = new ArrayList<>(Collections.singletonList(nodeWithEnvelope)); - monitor.beforeMergeTree(child.node, insert); - mergeTwoSubtrees(tx, child, insert); - monitor.afterMergeTree(child.node); - } else { - insertIndexNodeOnParent(tx, child.node, newRootNode); - } - } - - } else { - Node newRootNode = tx.createNode(); - buildRtreeFromScratch(tx, newRootNode, cluster, loadingFactor); - int newHeight = getHeight(newRootNode, 0); - if (newHeight == 1) { - monitor.addCase("h_i > l_t (d==1)"); - try (var relationships = newRootNode.getRelationships(RTreeRelationshipTypes.RTREE_REFERENCE)) { - for (Relationship geom : relationships) { - addBelow(tx, child.node, geom.getEndNode()); - geom.delete(); - } - } - } else { - monitor.addCase("h_i > l_t (d>1)"); - int insertDepth = newHeight - (currentRTreeHeight); - List childrenToBeInserted = getIndexChildren(newRootNode, insertDepth); - for (NodeWithEnvelope n : childrenToBeInserted) { - Relationship relationship = n.node.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING); - relationship.delete(); - if (!shouldMergeTrees) { - insertIndexNodeOnParent(tx, child.node, n.node); - } - } - if (shouldMergeTrees) { - monitor.beforeMergeTree(child.node, childrenToBeInserted); - mergeTwoSubtrees(tx, child, childrenToBeInserted); - monitor.afterMergeTree(child.node); - } - } - // todo wouldn't it be better for this temporary tree to only live in memory? - deleteRecursivelySubtree(newRootNode, null); // remove the buffer tree remnants - } - } - monitor.addSplit(rootNode); // for debugging via images - - return outliers; - } - - static class NodeTuple { - private final double overlap; - NodeWithEnvelope left; - NodeWithEnvelope right; - - NodeTuple(NodeWithEnvelope left, NodeWithEnvelope right) { - this.left = left; - this.right = right; - this.overlap = left.envelope.overlap(right.envelope); - } - - boolean contains(NodeWithEnvelope entry) { - return left.node.equals(entry.node) || right.node.equals(entry.node); - } - } - - protected void mergeTwoSubtrees(Transaction tx, NodeWithEnvelope parent, List right) { - ArrayList pairs = new ArrayList<>(); - HashSet disconnectedChildren = new HashSet<>(); - List left = getIndexChildren(parent.node); - for (NodeWithEnvelope leftNode : left) { - for (NodeWithEnvelope rightNode : right) { - NodeTuple pair = new NodeTuple(leftNode, rightNode); - if (pair.overlap > 0.1) { - pairs.add(pair); - } - } - } - pairs.sort(Comparator.comparingDouble(o -> o.overlap)); - while (!pairs.isEmpty()) { - NodeTuple pair = pairs.remove(pairs.size() - 1); - Envelope merged = new Envelope(pair.left.envelope); - merged.expandToInclude(pair.right.envelope); - NodeWithEnvelope newNode = new NodeWithEnvelope(pair.left.node, merged); - setIndexNodeEnvelope(newNode.node, newNode.envelope); - List rightChildren = getIndexChildren(pair.right.node); - pairs.removeIf(t -> t.contains(pair.left) || t.contains(pair.right)); - try (var relationships = pair.right.node.getRelationships()) { - for (Relationship rel : relationships) { - rel.delete(); - } - } - disconnectedChildren.add(pair.right); - mergeTwoSubtrees(tx, newNode, rightChildren); - } - - right.removeIf(disconnectedChildren::contains); - disconnectedChildren.forEach(t -> t.node.delete()); - - for (NodeWithEnvelope n : right) { - n.node.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING); - parent.node.createRelationshipTo(n.node, RTreeRelationshipTypes.RTREE_CHILD); - parent.envelope.expandToInclude(n.envelope); - } - setIndexNodeEnvelope(parent.node, parent.envelope); - if (countChildren(parent.node, RTreeRelationshipTypes.RTREE_CHILD) > maxNodeReferences) { - splitAndAdjustPathBoundingBox(tx, parent.node); - } else { - adjustPathBoundingBox(parent.node); - } - } - - private int expectedHeight(double loadingFactor, int size) { - if (size == 1) { - return 1; - } else { - final int targetLoading = (int) Math.floor(maxNodeReferences * loadingFactor); - return (int) Math.ceil(Math.log(size) / Math.log(targetLoading)); //exploit change of base formula - } - - } - - /** - * This algorithm is based on Overlap Minimizing Top-down Bulk Loading Algorithm for R-tree by T Lee and S Lee. - * This is effectively a wrapper function around the function Partition which will attempt to parallelise the task. - * This can work better or worse since the top level may have as few as two nodes, in which case it fails is not optimal. - * The loadingFactor must be between 0.1 and 1, this is how full each node will be, approximately. - * Use 1 for static trees (will not be added to after build built), lower numbers if there are to be many subsequent updates. - * //TODO - Better parallelisation strategy. - */ - private void buildRtreeFromScratch(Transaction tx, Node rootNode, final List geomNodes, double loadingFactor) { - partition(tx, rootNode, geomNodes, 0, loadingFactor); - } - - /** - * This will partition a collection of nodes under the specified index node. The nodes are clustered into one - * or more groups based on the loading factor, and the tree is expanded if necessary. If the nodes all fit - * into the parent, they are added directly, otherwise the depth is increased and partition called for each - * cluster at the deeper depth based on a new root node for each cluster. - */ - private void partition(Transaction tx, Node indexNode, List nodes, int depth, final double loadingFactor) { - - // We want to split by the longest dimension to avoid degrading into extremely thin envelopes - int longestDimension = findLongestDimension(nodes); - - // Sort the entries by the longest dimension and then create envelopes around left and right halves - nodes.sort(new SingleDimensionNodeEnvelopeComparator(longestDimension)); - - //work out the number of times to partition it: - final int targetLoading = (int) Math.round(maxNodeReferences * loadingFactor); - int nodeCount = nodes.size(); - - if (nodeCount <= targetLoading) { - // We have few enough nodes to add them directly to the current index node - boolean expandRootNodeBoundingBox = false; - for (NodeWithEnvelope n : nodes) { - expandRootNodeBoundingBox |= insertInLeaf(indexNode, n.node); - } - if (expandRootNodeBoundingBox) { - adjustPathBoundingBox(indexNode); - } - } else { - // We have more geometries than can fit in the current index node - create clusters and index them - final int height = expectedHeight(loadingFactor, nodeCount); //exploit change of base formula - final int subTreeSize = (int) Math.round(Math.pow(targetLoading, height - 1)); - final int numberOfPartitions = (int) Math.ceil((double) nodeCount / (double) subTreeSize); - // - TODO change this to use the sort function above - List> partitions = partitionList(nodes, numberOfPartitions); - - //recurse on each partition - for (List partition : partitions) { - Node newIndexNode = tx.createNode(); - if (partition.size() > 1) { - partition(tx, newIndexNode, partition, depth + 1, loadingFactor); - } else { - addBelow(tx, newIndexNode, partition.get(0).node); - } - insertIndexNodeOnParent(tx, indexNode, newIndexNode); - } - monitor.addSplit(indexNode); - } - } - - // quick dirty way to partition a set into equal sized disjoint subsets - // - TODO why not use list.sublist() without copying ? - - private List> partitionList(List nodes, int numberOfPartitions) { - int nodeCount = nodes.size(); - List> partitions = new ArrayList<>(numberOfPartitions); - - int partitionSize = nodeCount / numberOfPartitions; //it is critical that partitionSize is always less than the target loading. - if (nodeCount % numberOfPartitions > 0) { - partitionSize++; - } - for (int i = 0; i < numberOfPartitions; i++) { - partitions.add(nodes.subList(i * partitionSize, Math.min((i + 1) * partitionSize, nodeCount))); - } - return partitions; - } - - @Override - public void remove(Transaction tx, String geomNodeId, boolean deleteGeomNode, boolean throwExceptionIfNotFound) { - Node geomNode = null; - // getNodeByElementId throws NotFoundException if node is already removed - try { - geomNode = tx.getNodeByElementId(geomNodeId); - - } catch (NotFoundException nfe) { - - // propagate exception only if flag is set - if (throwExceptionIfNotFound) { - throw nfe; - } - } - if (geomNode != null && isGeometryNodeIndexed(geomNode)) { - - Node indexNode = findLeafContainingGeometryNode(geomNode); - - // be sure geomNode is inside this RTree - if (isIndexNodeInThisIndex(tx, indexNode)) { - - // remove the entry - final Relationship geometryRtreeReference = geomNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING); - if (geometryRtreeReference != null) { - geometryRtreeReference.delete(); - } - if (deleteGeomNode) { - deleteNode(geomNode); - } - - // reorganize the tree if needed - if (countChildren(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE) == 0) { - indexNode = deleteEmptyTreeNodes(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); - adjustParentBoundingBox(indexNode, RTreeRelationshipTypes.RTREE_CHILD); - } else { - adjustParentBoundingBox(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); - } - - adjustPathBoundingBox(indexNode); - - countSaved = false; - totalGeometryCount--; - } else if (throwExceptionIfNotFound) { - throw new RuntimeException("GeometryNode not indexed in this RTree: " + geomNodeId); - } - } else if (throwExceptionIfNotFound) { - throw new RuntimeException("GeometryNode not indexed with an RTree: " + geomNodeId); - } - } - - private Node deleteEmptyTreeNodes(Node indexNode, RelationshipType relType) { - if (countChildren(indexNode, relType) == 0) { - Node parent = getIndexNodeParent(indexNode); - if (parent != null) { - indexNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING).delete(); - - indexNode.delete(); - return deleteEmptyTreeNodes(parent, RTreeRelationshipTypes.RTREE_CHILD); - } else { - // root - return indexNode; - } - } else { - return indexNode; - } - } - - private void detachGeometryNodes(Transaction tx, final boolean deleteGeomNodes, Node indexRoot, final Listener monitor) { - monitor.begin(count(tx)); - try { - // delete all geometry nodes - visitInTx(tx, new SpatialIndexVisitor() { - @Override - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return true; - } - - @Override - public void onIndexReference(Node geomNode) { - geomNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING).delete(); - if (deleteGeomNodes) { - deleteNode(geomNode); - } - - monitor.worked(1); - } - }, indexRoot.getElementId()); - } finally { - monitor.done(); - } - } - - @Override - public void removeAll(Transaction tx, final boolean deleteGeomNodes, final Listener monitor) { - Node indexRoot = getIndexRoot(tx); - - detachGeometryNodes(tx, deleteGeomNodes, indexRoot, monitor); - - // delete index root relationship - indexRoot.getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.INCOMING).delete(); - - // delete tree - deleteRecursivelySubtree(indexRoot, null); - - // delete tree metadata - Relationship metadataNodeRelationship = getRootNode(tx).getSingleRelationship(RTreeRelationshipTypes.RTREE_METADATA, Direction.OUTGOING); - Node metadataNode = metadataNodeRelationship.getEndNode(); - metadataNodeRelationship.delete(); - metadataNode.delete(); - - countSaved = false; - totalGeometryCount = 0; - } - - @Override - public void clear(Transaction tx, final Listener monitor) { - removeAll(tx, false, new NullListener()); - initIndexRoot(tx); - initIndexMetadata(tx); - } - - @Override - public Envelope getBoundingBox(Transaction tx) { - return getIndexNodeEnvelope(getIndexRoot(tx)); - } - - @Override - public int count(Transaction tx) { - saveCount(tx); - return totalGeometryCount; - } - - @Override - public boolean isEmpty(Transaction tx) { - Node indexRoot = getIndexRoot(tx); - return !indexRoot.hasProperty(INDEX_PROP_BBOX); - } - - @Override - public boolean isNodeIndexed(Transaction tx, String geomNodeId) { - Node geomNode = tx.getNodeByElementId(geomNodeId); - // be sure geomNode is inside this RTree - return geomNode != null && isGeometryNodeIndexed(geomNode) && isIndexNodeInThisIndex(tx, findLeafContainingGeometryNode(geomNode)); - } - - public void warmUp(Transaction tx) { - visit(tx, new WarmUpVisitor(), getIndexRoot(tx)); - } - - public Iterable getAllIndexInternalNodes(Transaction tx) { - MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); - TraversalDescription td = traversal - .breadthFirst() - .relationships(RTreeRelationshipTypes.RTREE_CHILD, Direction.OUTGOING) - .evaluator(Evaluators.all()); - return td.traverse(getIndexRoot(tx)).nodes(); - } - - @Override - public Iterable getAllIndexedNodes(Transaction tx) { - return new IndexNodeToGeometryNodeIterable(getAllIndexInternalNodes(tx)); - } - - private class SearchEvaluator implements Evaluator { - private final SearchFilter filter; - private final Transaction tx; - - public SearchEvaluator(Transaction tx, SearchFilter filter) { - this.tx = tx; - this.filter = filter; - } - - @Override - public Evaluation evaluate(Path path) { - Relationship rel = path.lastRelationship(); - Node node = path.endNode(); - if (rel == null) { - return Evaluation.EXCLUDE_AND_CONTINUE; - } else if (rel.isType(RTreeRelationshipTypes.RTREE_CHILD)) { - boolean shouldContinue = filter.needsToVisit(getIndexNodeEnvelope(node)); - if (shouldContinue) monitor.matchedTreeNode(path.length(), node); - monitor.addCase(shouldContinue ? "Index Matches" : "Index Does NOT Match"); - return shouldContinue ? - Evaluation.EXCLUDE_AND_CONTINUE : - Evaluation.EXCLUDE_AND_PRUNE; - } else if (rel.isType(RTreeRelationshipTypes.RTREE_REFERENCE)) { - boolean found = filter.geometryMatches(tx, node); - monitor.addCase(found ? "Geometry Matches" : "Geometry Does NOT Match"); - if (found) monitor.setHeight(path.length()); - return found ? - Evaluation.INCLUDE_AND_PRUNE : - Evaluation.EXCLUDE_AND_PRUNE; - } - return null; - } - } - - @Override - public SearchResults searchIndex(Transaction tx, SearchFilter filter) { - SearchEvaluator searchEvaluator = new SearchEvaluator(tx, filter); - MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); - TraversalDescription td = traversal - .depthFirst() - .relationships(RTreeRelationshipTypes.RTREE_CHILD, Direction.OUTGOING) - .relationships(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.OUTGOING) - .evaluator(searchEvaluator); - Traverser traverser = td.traverse(getIndexRoot(tx)); - return new SearchResults(traverser.nodes()); - } - - public void visit(Transaction tx, SpatialIndexVisitor visitor, Node indexNode) { - if (!visitor.needsToVisit(getIndexNodeEnvelope(indexNode))) { - return; - } - - if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - // Node is not a leaf - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship rel : relationships) { - Node child = rel.getEndNode(); - // collect children results - visit(tx, visitor, child); - } - } - } else if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { - // Node is a leaf - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { - for (Relationship rel : relationships) { - visitor.onIndexReference(rel.getEndNode()); - } - } - } - } - - public Node getIndexRoot(Transaction tx) { - return getRootNode(tx).getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.OUTGOING).getEndNode(); - } - - // Private methods - - /*** - * This will get the envelope of the child. The relationshipType acts as as flag to allow the function to - * know whether the child is a leaf or an index node. - */ - private Envelope getChildNodeEnvelope(Node child, RelationshipType relType) { - if (relType.name().equals(RTreeRelationshipTypes.RTREE_REFERENCE.name())) { - return getLeafNodeEnvelope(child); - } else { - return getIndexNodeEnvelope(child); - } - } - - /** - * The leaf nodes belong to the domain model, and as such need to use - * the layers domain-specific GeometryEncoder for decoding the envelope. - */ - public Envelope getLeafNodeEnvelope(Node geomNode) { - return envelopeDecoder.decodeEnvelope(geomNode); - } - - /** - * The index nodes do NOT belong to the domain model, and as such need - * to use the indexes internal knowledge of the index tree and node - * structure for decoding the envelope. - */ - public Envelope getIndexNodeEnvelope(Node indexNode) { - if (!indexNode.hasProperty(INDEX_PROP_BBOX)) { - // this is ok after an index node split - return null; - } - - double[] bbox = (double[]) indexNode.getProperty(INDEX_PROP_BBOX); - // Envelope parameters: xmin, xmax, ymin, ymax - return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); - } - - private void visitInTx(Transaction tx, SpatialIndexVisitor visitor, String indexNodeId) { - Node indexNode = tx.getNodeByElementId(indexNodeId); - if (!visitor.needsToVisit(getIndexNodeEnvelope(indexNode))) { - return; - } - - if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - // Node is not a leaf - - // collect children - List children = new ArrayList<>(); - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship rel : relationships) { - children.add(rel.getEndNode().getElementId()); - } - } - - - // visit children - for (String child : children) { - visitInTx(tx, visitor, child); - } - } else if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { - // Node is a leaf - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { - for (Relationship rel : relationships) { - visitor.onIndexReference(rel.getEndNode()); - } - } - } - } - - private void initIndexMetadata(Transaction tx) { - Node layerNode = getRootNode(tx); - if (layerNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_METADATA)) { - // metadata already present - Node metadataNode = layerNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_METADATA, Direction.OUTGOING).getEndNode(); - - maxNodeReferences = (Integer) metadataNode.getProperty("maxNodeReferences"); - } else { - // metadata initialization - Node metadataNode = tx.createNode(); - layerNode.createRelationshipTo(metadataNode, RTreeRelationshipTypes.RTREE_METADATA); - - metadataNode.setProperty("maxNodeReferences", maxNodeReferences); - } - - saveCount(tx); - } - - private void initIndexRoot(Transaction tx) { - Node layerNode = getRootNode(tx); - if (!layerNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_ROOT)) { - // index initialization - Node root = tx.createNode(); - layerNode.createRelationshipTo(root, RTreeRelationshipTypes.RTREE_ROOT); - } - } - - private Node getMetadataNode(Transaction tx) { - return getRootNode(tx).getSingleRelationship(RTreeRelationshipTypes.RTREE_METADATA, Direction.OUTGOING).getEndNode(); - } - - /** - * Save the geometry count to the database if it has not been saved yet. - * However, if the count is zero, first do an exhaustive search of the - * tree and count everything before saving it. - */ - private void saveCount(Transaction tx) { - if (totalGeometryCount == 0) { - SpatialIndexRecordCounter counter = new SpatialIndexRecordCounter(); - visit(tx, counter, getIndexRoot(tx)); - totalGeometryCount = counter.getResult(); - - int savedGeometryCount = (int) getMetadataNode(tx).getProperty("totalGeometryCount", 0); - countSaved = savedGeometryCount == totalGeometryCount; - } - - if (!countSaved) { - getMetadataNode(tx).setProperty("totalGeometryCount", totalGeometryCount); - countSaved = true; - } - } - - private boolean nodeIsLeaf(Node node) { - return !node.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD); - } - - private Node chooseSubTree(Node parentIndexNode, Node geomRootNode) { - // children that can contain the new geometry - List indexNodes = new ArrayList<>(); - - // pick the child that contains the new geometry bounding box - try (var relationships = parentIndexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship relation : relationships) { - Node indexNode = relation.getEndNode(); - if (getIndexNodeEnvelope(indexNode).contains(getLeafNodeEnvelope(geomRootNode))) { - indexNodes.add(indexNode); - } - } - } - - if (indexNodes.size() > 1) { - return chooseIndexNodeWithSmallestArea(indexNodes); - } else if (indexNodes.size() == 1) { - return indexNodes.get(0); - } - - // pick the child that needs the minimum enlargement to include the new geometry - double minimumEnlargement = Double.POSITIVE_INFINITY; - try (var relationships = parentIndexNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship relation : relationships) { - Node indexNode = relation.getEndNode(); - double enlargementNeeded = getAreaEnlargement(indexNode, geomRootNode); - - if (enlargementNeeded < minimumEnlargement) { - indexNodes.clear(); - indexNodes.add(indexNode); - minimumEnlargement = enlargementNeeded; - } else if (enlargementNeeded == minimumEnlargement) { - indexNodes.add(indexNode); - } - } - } - - if (indexNodes.size() > 1) { - return chooseIndexNodeWithSmallestArea(indexNodes); - } else if (indexNodes.size() == 1) { - return indexNodes.get(0); - } else { - // this shouldn't happen - throw new RuntimeException("No IndexNode found for new geometry"); - } - } - - private double getAreaEnlargement(Node indexNode, Node geomRootNode) { - Envelope before = getIndexNodeEnvelope(indexNode); - - Envelope after = getLeafNodeEnvelope(geomRootNode); - after.expandToInclude(before); - - return getArea(after) - getArea(before); - } - - private Node chooseIndexNodeWithSmallestArea(List indexNodes) { - Node result = null; - double smallestArea = -1; - - for (Node indexNode : indexNodes) { - double area = getArea(getIndexNodeEnvelope(indexNode)); - if (result == null || area < smallestArea) { - result = indexNode; - smallestArea = area; - } - } - - return result; - } - - private int countChildren(Node indexNode, RelationshipType relationshipType) { - int counter = 0; - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { - for (Relationship ignored : relationships) { - counter++; - } - } - return counter; - } - - /** - * @return is enlargement needed? - */ - private boolean insertInLeaf(Node indexNode, Node geomRootNode) { - return addChild(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE, geomRootNode); - } - - private void splitAndAdjustPathBoundingBox(Transaction tx, Node indexNode) { - // create a new node and distribute the entries - Node newIndexNode = splitMode.equals(GREENES_SPLIT) ? greenesSplit(tx, indexNode) : quadraticSplit(tx, indexNode); - Node parent = getIndexNodeParent(indexNode); + if (expectedHeight < currentRTreeHeight) { + monitor.addCase("h_i < l_t "); + //if the height is smaller than that recursively sort and split. + outliers.addAll(bulkInsertion(tx, child.node, rootNodeHeight - 1, cluster, loadingFactor)); + } //if constructed tree is the correct size insert it here. + else if (expectedHeight == currentRTreeHeight) { + + //Do not create underfull nodes, instead use the add logic, except we know the root not to add them too. + //this handles the case where the number of nodes in a cluster is small. + + if (cluster.size() < maxNodeReferences * loadingFactor / 2) { + monitor.addCase("h_i == l_t && small cluster"); + // getParent because addition might cause a split. This strategy not ideal, + // but does tend to limit overlap more than adding to the child exclusively. + + for (NodeWithEnvelope n : cluster) { + addBelow(tx, rootNode, n.node); + } + } else { + monitor.addCase("h_i == l_t && big cluster"); + Node newRootNode = tx.createNode(); + buildRtreeFromScratch(tx, newRootNode, cluster, loadingFactor); + if (shouldMergeTrees) { + NodeWithEnvelope nodeWithEnvelope = new NodeWithEnvelope(newRootNode, + getIndexNodeEnvelope(newRootNode)); + List insert = new ArrayList<>(Collections.singletonList(nodeWithEnvelope)); + monitor.beforeMergeTree(child.node, insert); + mergeTwoSubtrees(tx, child, insert); + monitor.afterMergeTree(child.node); + } else { + insertIndexNodeOnParent(tx, child.node, newRootNode); + } + } + + } else { + Node newRootNode = tx.createNode(); + buildRtreeFromScratch(tx, newRootNode, cluster, loadingFactor); + int newHeight = getHeight(newRootNode, 0); + if (newHeight == 1) { + monitor.addCase("h_i > l_t (d==1)"); + try (var relationships = newRootNode.getRelationships(RTreeRelationshipTypes.RTREE_REFERENCE)) { + for (Relationship geom : relationships) { + addBelow(tx, child.node, geom.getEndNode()); + geom.delete(); + } + } + } else { + monitor.addCase("h_i > l_t (d>1)"); + int insertDepth = newHeight - (currentRTreeHeight); + List childrenToBeInserted = getIndexChildren(newRootNode, insertDepth); + for (NodeWithEnvelope n : childrenToBeInserted) { + Relationship relationship = n.node.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, + Direction.INCOMING); + relationship.delete(); + if (!shouldMergeTrees) { + insertIndexNodeOnParent(tx, child.node, n.node); + } + } + if (shouldMergeTrees) { + monitor.beforeMergeTree(child.node, childrenToBeInserted); + mergeTwoSubtrees(tx, child, childrenToBeInserted); + monitor.afterMergeTree(child.node); + } + } + // todo wouldn't it be better for this temporary tree to only live in memory? + deleteRecursivelySubtree(newRootNode, null); // remove the buffer tree remnants + } + } + monitor.addSplit(rootNode); // for debugging via images + + return outliers; + } + + static class NodeTuple { + + private final double overlap; + NodeWithEnvelope left; + NodeWithEnvelope right; + + NodeTuple(NodeWithEnvelope left, NodeWithEnvelope right) { + this.left = left; + this.right = right; + this.overlap = left.envelope.overlap(right.envelope); + } + + boolean contains(NodeWithEnvelope entry) { + return left.node.equals(entry.node) || right.node.equals(entry.node); + } + } + + protected void mergeTwoSubtrees(Transaction tx, NodeWithEnvelope parent, List right) { + ArrayList pairs = new ArrayList<>(); + HashSet disconnectedChildren = new HashSet<>(); + List left = getIndexChildren(parent.node); + for (NodeWithEnvelope leftNode : left) { + for (NodeWithEnvelope rightNode : right) { + NodeTuple pair = new NodeTuple(leftNode, rightNode); + if (pair.overlap > 0.1) { + pairs.add(pair); + } + } + } + pairs.sort(Comparator.comparingDouble(o -> o.overlap)); + while (!pairs.isEmpty()) { + NodeTuple pair = pairs.remove(pairs.size() - 1); + Envelope merged = new Envelope(pair.left.envelope); + merged.expandToInclude(pair.right.envelope); + NodeWithEnvelope newNode = new NodeWithEnvelope(pair.left.node, merged); + setIndexNodeEnvelope(newNode.node, newNode.envelope); + List rightChildren = getIndexChildren(pair.right.node); + pairs.removeIf(t -> t.contains(pair.left) || t.contains(pair.right)); + try (var relationships = pair.right.node.getRelationships()) { + for (Relationship rel : relationships) { + rel.delete(); + } + } + disconnectedChildren.add(pair.right); + mergeTwoSubtrees(tx, newNode, rightChildren); + } + + right.removeIf(disconnectedChildren::contains); + disconnectedChildren.forEach(t -> t.node.delete()); + + for (NodeWithEnvelope n : right) { + n.node.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING); + parent.node.createRelationshipTo(n.node, RTreeRelationshipTypes.RTREE_CHILD); + parent.envelope.expandToInclude(n.envelope); + } + setIndexNodeEnvelope(parent.node, parent.envelope); + if (countChildren(parent.node, RTreeRelationshipTypes.RTREE_CHILD) > maxNodeReferences) { + splitAndAdjustPathBoundingBox(tx, parent.node); + } else { + adjustPathBoundingBox(parent.node); + } + } + + private int expectedHeight(double loadingFactor, int size) { + if (size == 1) { + return 1; + } else { + final int targetLoading = (int) Math.floor(maxNodeReferences * loadingFactor); + return (int) Math.ceil(Math.log(size) / Math.log(targetLoading)); //exploit change of base formula + } + + } + + /** + * This algorithm is based on Overlap Minimizing Top-down Bulk Loading Algorithm for R-tree by T Lee and S Lee. + * This is effectively a wrapper function around the function Partition which will attempt to parallelise the task. + * This can work better or worse since the top level may have as few as two nodes, in which case it fails is not + * optimal. + * The loadingFactor must be between 0.1 and 1, this is how full each node will be, approximately. + * Use 1 for static trees (will not be added to after build built), lower numbers if there are to be many subsequent + * updates. + * //TODO - Better parallelisation strategy. + */ + private void buildRtreeFromScratch(Transaction tx, Node rootNode, final List geomNodes, + double loadingFactor) { + partition(tx, rootNode, geomNodes, 0, loadingFactor); + } + + /** + * This will partition a collection of nodes under the specified index node. The nodes are clustered into one + * or more groups based on the loading factor, and the tree is expanded if necessary. If the nodes all fit + * into the parent, they are added directly, otherwise the depth is increased and partition called for each + * cluster at the deeper depth based on a new root node for each cluster. + */ + private void partition(Transaction tx, Node indexNode, List nodes, int depth, + final double loadingFactor) { + + // We want to split by the longest dimension to avoid degrading into extremely thin envelopes + int longestDimension = findLongestDimension(nodes); + + // Sort the entries by the longest dimension and then create envelopes around left and right halves + nodes.sort(new SingleDimensionNodeEnvelopeComparator(longestDimension)); + + //work out the number of times to partition it: + final int targetLoading = (int) Math.round(maxNodeReferences * loadingFactor); + int nodeCount = nodes.size(); + + if (nodeCount <= targetLoading) { + // We have few enough nodes to add them directly to the current index node + boolean expandRootNodeBoundingBox = false; + for (NodeWithEnvelope n : nodes) { + expandRootNodeBoundingBox |= insertInLeaf(indexNode, n.node); + } + if (expandRootNodeBoundingBox) { + adjustPathBoundingBox(indexNode); + } + } else { + // We have more geometries than can fit in the current index node - create clusters and index them + final int height = expectedHeight(loadingFactor, nodeCount); //exploit change of base formula + final int subTreeSize = (int) Math.round(Math.pow(targetLoading, height - 1)); + final int numberOfPartitions = (int) Math.ceil((double) nodeCount / (double) subTreeSize); + // - TODO change this to use the sort function above + List> partitions = partitionList(nodes, numberOfPartitions); + + //recurse on each partition + for (List partition : partitions) { + Node newIndexNode = tx.createNode(); + if (partition.size() > 1) { + partition(tx, newIndexNode, partition, depth + 1, loadingFactor); + } else { + addBelow(tx, newIndexNode, partition.get(0).node); + } + insertIndexNodeOnParent(tx, indexNode, newIndexNode); + } + monitor.addSplit(indexNode); + } + } + + // quick dirty way to partition a set into equal sized disjoint subsets + // - TODO why not use list.sublist() without copying ? + + private List> partitionList(List nodes, int numberOfPartitions) { + int nodeCount = nodes.size(); + List> partitions = new ArrayList<>(numberOfPartitions); + + int partitionSize = nodeCount + / numberOfPartitions; //it is critical that partitionSize is always less than the target loading. + if (nodeCount % numberOfPartitions > 0) { + partitionSize++; + } + for (int i = 0; i < numberOfPartitions; i++) { + partitions.add(nodes.subList(i * partitionSize, Math.min((i + 1) * partitionSize, nodeCount))); + } + return partitions; + } + + @Override + public void remove(Transaction tx, String geomNodeId, boolean deleteGeomNode, boolean throwExceptionIfNotFound) { + Node geomNode = null; + // getNodeByElementId throws NotFoundException if node is already removed + try { + geomNode = tx.getNodeByElementId(geomNodeId); + + } catch (NotFoundException nfe) { + + // propagate exception only if flag is set + if (throwExceptionIfNotFound) { + throw nfe; + } + } + if (geomNode != null && isGeometryNodeIndexed(geomNode)) { + + Node indexNode = findLeafContainingGeometryNode(geomNode); + + // be sure geomNode is inside this RTree + if (isIndexNodeInThisIndex(tx, indexNode)) { + + // remove the entry + final Relationship geometryRtreeReference = geomNode.getSingleRelationship( + RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING); + if (geometryRtreeReference != null) { + geometryRtreeReference.delete(); + } + if (deleteGeomNode) { + deleteNode(geomNode); + } + + // reorganize the tree if needed + if (countChildren(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE) == 0) { + indexNode = deleteEmptyTreeNodes(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); + adjustParentBoundingBox(indexNode, RTreeRelationshipTypes.RTREE_CHILD); + } else { + adjustParentBoundingBox(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); + } + + adjustPathBoundingBox(indexNode); + + countSaved = false; + totalGeometryCount--; + } else if (throwExceptionIfNotFound) { + throw new RuntimeException("GeometryNode not indexed in this RTree: " + geomNodeId); + } + } else if (throwExceptionIfNotFound) { + throw new RuntimeException("GeometryNode not indexed with an RTree: " + geomNodeId); + } + } + + private Node deleteEmptyTreeNodes(Node indexNode, RelationshipType relType) { + if (countChildren(indexNode, relType) == 0) { + Node parent = getIndexNodeParent(indexNode); + if (parent != null) { + indexNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING).delete(); + + indexNode.delete(); + return deleteEmptyTreeNodes(parent, RTreeRelationshipTypes.RTREE_CHILD); + } else { + // root + return indexNode; + } + } else { + return indexNode; + } + } + + private void detachGeometryNodes(Transaction tx, final boolean deleteGeomNodes, Node indexRoot, + final Listener monitor) { + monitor.begin(count(tx)); + try { + // delete all geometry nodes + visitInTx(tx, new SpatialIndexVisitor() { + @Override + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return true; + } + + @Override + public void onIndexReference(Node geomNode) { + geomNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING).delete(); + if (deleteGeomNodes) { + deleteNode(geomNode); + } + + monitor.worked(1); + } + }, indexRoot.getElementId()); + } finally { + monitor.done(); + } + } + + @Override + public void removeAll(Transaction tx, final boolean deleteGeomNodes, final Listener monitor) { + Node indexRoot = getIndexRoot(tx); + + detachGeometryNodes(tx, deleteGeomNodes, indexRoot, monitor); + + // delete index root relationship + indexRoot.getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.INCOMING).delete(); + + // delete tree + deleteRecursivelySubtree(indexRoot, null); + + // delete tree metadata + Relationship metadataNodeRelationship = getRootNode(tx).getSingleRelationship( + RTreeRelationshipTypes.RTREE_METADATA, Direction.OUTGOING); + Node metadataNode = metadataNodeRelationship.getEndNode(); + metadataNodeRelationship.delete(); + metadataNode.delete(); + + countSaved = false; + totalGeometryCount = 0; + } + + @Override + public void clear(Transaction tx, final Listener monitor) { + removeAll(tx, false, new NullListener()); + initIndexRoot(tx); + initIndexMetadata(tx); + } + + @Override + public Envelope getBoundingBox(Transaction tx) { + return getIndexNodeEnvelope(getIndexRoot(tx)); + } + + @Override + public int count(Transaction tx) { + saveCount(tx); + return totalGeometryCount; + } + + @Override + public boolean isEmpty(Transaction tx) { + Node indexRoot = getIndexRoot(tx); + return !indexRoot.hasProperty(INDEX_PROP_BBOX); + } + + @Override + public boolean isNodeIndexed(Transaction tx, String geomNodeId) { + Node geomNode = tx.getNodeByElementId(geomNodeId); + // be sure geomNode is inside this RTree + return geomNode != null && isGeometryNodeIndexed(geomNode) && isIndexNodeInThisIndex(tx, + findLeafContainingGeometryNode(geomNode)); + } + + public void warmUp(Transaction tx) { + visit(tx, new WarmUpVisitor(), getIndexRoot(tx)); + } + + public Iterable getAllIndexInternalNodes(Transaction tx) { + MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); + TraversalDescription td = traversal + .breadthFirst() + .relationships(RTreeRelationshipTypes.RTREE_CHILD, Direction.OUTGOING) + .evaluator(Evaluators.all()); + return td.traverse(getIndexRoot(tx)).nodes(); + } + + @Override + public Iterable getAllIndexedNodes(Transaction tx) { + return new IndexNodeToGeometryNodeIterable(getAllIndexInternalNodes(tx)); + } + + private class SearchEvaluator implements Evaluator { + + private final SearchFilter filter; + private final Transaction tx; + + public SearchEvaluator(Transaction tx, SearchFilter filter) { + this.tx = tx; + this.filter = filter; + } + + @Override + public Evaluation evaluate(Path path) { + Relationship rel = path.lastRelationship(); + Node node = path.endNode(); + if (rel == null) { + return Evaluation.EXCLUDE_AND_CONTINUE; + } else if (rel.isType(RTreeRelationshipTypes.RTREE_CHILD)) { + boolean shouldContinue = filter.needsToVisit(getIndexNodeEnvelope(node)); + if (shouldContinue) { + monitor.matchedTreeNode(path.length(), node); + } + monitor.addCase(shouldContinue ? "Index Matches" : "Index Does NOT Match"); + return shouldContinue ? + Evaluation.EXCLUDE_AND_CONTINUE : + Evaluation.EXCLUDE_AND_PRUNE; + } else if (rel.isType(RTreeRelationshipTypes.RTREE_REFERENCE)) { + boolean found = filter.geometryMatches(tx, node); + monitor.addCase(found ? "Geometry Matches" : "Geometry Does NOT Match"); + if (found) { + monitor.setHeight(path.length()); + } + return found ? + Evaluation.INCLUDE_AND_PRUNE : + Evaluation.EXCLUDE_AND_PRUNE; + } + return null; + } + } + + @Override + public SearchResults searchIndex(Transaction tx, SearchFilter filter) { + SearchEvaluator searchEvaluator = new SearchEvaluator(tx, filter); + MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); + TraversalDescription td = traversal + .depthFirst() + .relationships(RTreeRelationshipTypes.RTREE_CHILD, Direction.OUTGOING) + .relationships(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.OUTGOING) + .evaluator(searchEvaluator); + Traverser traverser = td.traverse(getIndexRoot(tx)); + return new SearchResults(traverser.nodes()); + } + + public void visit(Transaction tx, SpatialIndexVisitor visitor, Node indexNode) { + if (!visitor.needsToVisit(getIndexNodeEnvelope(indexNode))) { + return; + } + + if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + // Node is not a leaf + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship rel : relationships) { + Node child = rel.getEndNode(); + // collect children results + visit(tx, visitor, child); + } + } + } else if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { + // Node is a leaf + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_REFERENCE)) { + for (Relationship rel : relationships) { + visitor.onIndexReference(rel.getEndNode()); + } + } + } + } + + public Node getIndexRoot(Transaction tx) { + return getRootNode(tx).getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.OUTGOING) + .getEndNode(); + } + + // Private methods + + /*** + * This will get the envelope of the child. The relationshipType acts as as flag to allow the function to + * know whether the child is a leaf or an index node. + */ + private Envelope getChildNodeEnvelope(Node child, RelationshipType relType) { + if (relType.name().equals(RTreeRelationshipTypes.RTREE_REFERENCE.name())) { + return getLeafNodeEnvelope(child); + } else { + return getIndexNodeEnvelope(child); + } + } + + /** + * The leaf nodes belong to the domain model, and as such need to use + * the layers domain-specific GeometryEncoder for decoding the envelope. + */ + public Envelope getLeafNodeEnvelope(Node geomNode) { + return envelopeDecoder.decodeEnvelope(geomNode); + } + + /** + * The index nodes do NOT belong to the domain model, and as such need + * to use the indexes internal knowledge of the index tree and node + * structure for decoding the envelope. + */ + public Envelope getIndexNodeEnvelope(Node indexNode) { + if (!indexNode.hasProperty(INDEX_PROP_BBOX)) { + // this is ok after an index node split + return null; + } + + double[] bbox = (double[]) indexNode.getProperty(INDEX_PROP_BBOX); + // Envelope parameters: xmin, xmax, ymin, ymax + return new Envelope(bbox[0], bbox[2], bbox[1], bbox[3]); + } + + private void visitInTx(Transaction tx, SpatialIndexVisitor visitor, String indexNodeId) { + Node indexNode = tx.getNodeByElementId(indexNodeId); + if (!visitor.needsToVisit(getIndexNodeEnvelope(indexNode))) { + return; + } + + if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + // Node is not a leaf + + // collect children + List children = new ArrayList<>(); + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship rel : relationships) { + children.add(rel.getEndNode().getElementId()); + } + } + + // visit children + for (String child : children) { + visitInTx(tx, visitor, child); + } + } else if (indexNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { + // Node is a leaf + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_REFERENCE)) { + for (Relationship rel : relationships) { + visitor.onIndexReference(rel.getEndNode()); + } + } + } + } + + private void initIndexMetadata(Transaction tx) { + Node layerNode = getRootNode(tx); + if (layerNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_METADATA)) { + // metadata already present + Node metadataNode = layerNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_METADATA, + Direction.OUTGOING).getEndNode(); + + maxNodeReferences = (Integer) metadataNode.getProperty("maxNodeReferences"); + } else { + // metadata initialization + Node metadataNode = tx.createNode(); + layerNode.createRelationshipTo(metadataNode, RTreeRelationshipTypes.RTREE_METADATA); + + metadataNode.setProperty("maxNodeReferences", maxNodeReferences); + } + + saveCount(tx); + } + + private void initIndexRoot(Transaction tx) { + Node layerNode = getRootNode(tx); + if (!layerNode.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_ROOT)) { + // index initialization + Node root = tx.createNode(); + layerNode.createRelationshipTo(root, RTreeRelationshipTypes.RTREE_ROOT); + } + } + + private Node getMetadataNode(Transaction tx) { + return getRootNode(tx).getSingleRelationship(RTreeRelationshipTypes.RTREE_METADATA, Direction.OUTGOING) + .getEndNode(); + } + + /** + * Save the geometry count to the database if it has not been saved yet. + * However, if the count is zero, first do an exhaustive search of the + * tree and count everything before saving it. + */ + private void saveCount(Transaction tx) { + if (totalGeometryCount == 0) { + SpatialIndexRecordCounter counter = new SpatialIndexRecordCounter(); + visit(tx, counter, getIndexRoot(tx)); + totalGeometryCount = counter.getResult(); + + int savedGeometryCount = (int) getMetadataNode(tx).getProperty("totalGeometryCount", 0); + countSaved = savedGeometryCount == totalGeometryCount; + } + + if (!countSaved) { + getMetadataNode(tx).setProperty("totalGeometryCount", totalGeometryCount); + countSaved = true; + } + } + + private boolean nodeIsLeaf(Node node) { + return !node.hasRelationship(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD); + } + + private Node chooseSubTree(Node parentIndexNode, Node geomRootNode) { + // children that can contain the new geometry + List indexNodes = new ArrayList<>(); + + // pick the child that contains the new geometry bounding box + try (var relationships = parentIndexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship relation : relationships) { + Node indexNode = relation.getEndNode(); + if (getIndexNodeEnvelope(indexNode).contains(getLeafNodeEnvelope(geomRootNode))) { + indexNodes.add(indexNode); + } + } + } + + if (indexNodes.size() > 1) { + return chooseIndexNodeWithSmallestArea(indexNodes); + } else if (indexNodes.size() == 1) { + return indexNodes.get(0); + } + + // pick the child that needs the minimum enlargement to include the new geometry + double minimumEnlargement = Double.POSITIVE_INFINITY; + try (var relationships = parentIndexNode.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship relation : relationships) { + Node indexNode = relation.getEndNode(); + double enlargementNeeded = getAreaEnlargement(indexNode, geomRootNode); + + if (enlargementNeeded < minimumEnlargement) { + indexNodes.clear(); + indexNodes.add(indexNode); + minimumEnlargement = enlargementNeeded; + } else if (enlargementNeeded == minimumEnlargement) { + indexNodes.add(indexNode); + } + } + } + + if (indexNodes.size() > 1) { + return chooseIndexNodeWithSmallestArea(indexNodes); + } else if (indexNodes.size() == 1) { + return indexNodes.get(0); + } else { + // this shouldn't happen + throw new RuntimeException("No IndexNode found for new geometry"); + } + } + + private double getAreaEnlargement(Node indexNode, Node geomRootNode) { + Envelope before = getIndexNodeEnvelope(indexNode); + + Envelope after = getLeafNodeEnvelope(geomRootNode); + after.expandToInclude(before); + + return getArea(after) - getArea(before); + } + + private Node chooseIndexNodeWithSmallestArea(List indexNodes) { + Node result = null; + double smallestArea = -1; + + for (Node indexNode : indexNodes) { + double area = getArea(getIndexNodeEnvelope(indexNode)); + if (result == null || area < smallestArea) { + result = indexNode; + smallestArea = area; + } + } + + return result; + } + + private int countChildren(Node indexNode, RelationshipType relationshipType) { + int counter = 0; + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { + for (Relationship ignored : relationships) { + counter++; + } + } + return counter; + } + + /** + * @return is enlargement needed? + */ + private boolean insertInLeaf(Node indexNode, Node geomRootNode) { + return addChild(indexNode, RTreeRelationshipTypes.RTREE_REFERENCE, geomRootNode); + } + + private void splitAndAdjustPathBoundingBox(Transaction tx, Node indexNode) { + // create a new node and distribute the entries + Node newIndexNode = + splitMode.equals(GREENES_SPLIT) ? greenesSplit(tx, indexNode) : quadraticSplit(tx, indexNode); + Node parent = getIndexNodeParent(indexNode); // System.out.println("spitIndex " + newIndexNode.getId()); // System.out.println("parent " + parent.getId()); - if (parent == null) { - // if indexNode is the root - createNewRoot(tx, indexNode, newIndexNode); - } else { - expandParentBoundingBoxAfterNewChild(parent, (double[]) indexNode.getProperty(INDEX_PROP_BBOX)); - - addChild(parent, RTreeRelationshipTypes.RTREE_CHILD, newIndexNode); - - if (countChildren(parent, RTreeRelationshipTypes.RTREE_CHILD) > maxNodeReferences) { - splitAndAdjustPathBoundingBox(tx, parent); - } else { - adjustPathBoundingBox(parent); - } - } - monitor.addSplit(newIndexNode); - } - - private Node quadraticSplit(Transaction tx, Node indexNode) { - if (nodeIsLeaf(indexNode)) { - return quadraticSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); - } else { - return quadraticSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_CHILD); - } - } - - private Node greenesSplit(Transaction tx, Node indexNode) { - if (nodeIsLeaf(indexNode)) { - return greenesSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); - } else { - return greenesSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_CHILD); - } - } - - private NodeWithEnvelope[] mostDistantByDeadSpace(List entries) { - NodeWithEnvelope seed1 = entries.get(0); - NodeWithEnvelope seed2 = entries.get(0); - double worst = Double.NEGATIVE_INFINITY; - for (int i = 0; i < entries.size(); ++i) { - NodeWithEnvelope e = entries.get(i); - for (int j = i + 1; j < entries.size(); ++j) { - NodeWithEnvelope e1 = entries.get(j); - double deadSpace = e.envelope.separation(e1.envelope); - if (deadSpace > worst) { - worst = deadSpace; - seed1 = e; - seed2 = e1; - } - } - } - return new NodeWithEnvelope[]{seed1, seed2}; - } - - private int findLongestDimension(List entries) { - if (entries.size() > 0) { - Envelope env = new Envelope(entries.get(0).envelope); - for (NodeWithEnvelope entry : entries) { - env.expandToInclude(entry.envelope); - } - int longestDimension = 0; - double maxWidth = Double.NEGATIVE_INFINITY; - for (int i = 0; i < env.getDimension(); i++) { - double width = env.getWidth(i); - if (width > maxWidth) { - maxWidth = width; - longestDimension = i; - } - } - return longestDimension; - } else { - return 0; - } - } - - private List extractChildNodesWithEnvelopes(Node indexNode, RelationshipType relationshipType) { - List entries = new ArrayList<>(); - - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { - for (Relationship relationship : relationships) { - Node node = relationship.getEndNode(); - entries.add(new NodeWithEnvelope(node, getChildNodeEnvelope(node, relationshipType))); - relationship.delete(); - } - } - return entries; - } - - private Node greenesSplit(Transaction tx, Node indexNode, RelationshipType relationshipType) { - // Disconnect all current children from the index and return them with their envelopes - List entries = extractChildNodesWithEnvelopes(indexNode, relationshipType); - - // We want to split by the longest dimension to avoid degrading into extremely thin envelopes - int longestDimension = findLongestDimension(entries); - - // Sort the entries by the longest dimension and then create envelopes around left and right halves - entries.sort(new SingleDimensionNodeEnvelopeComparator(longestDimension)); - int splitAt = entries.size() / 2; - List left = entries.subList(0, splitAt); - List right = entries.subList(splitAt, entries.size()); - - return reconnectTwoChildGroups(tx, indexNode, left, right, relationshipType); - } - - private static class SingleDimensionNodeEnvelopeComparator implements Comparator { - private final int dimension; - - public SingleDimensionNodeEnvelopeComparator(int dimension) { - this.dimension = dimension; - } - - @Override - public int compare(NodeWithEnvelope o1, NodeWithEnvelope o2) { - double length = o2.envelope.centre(dimension) - o1.envelope.centre(dimension); - return Double.compare(length, 0.0); - } - } - - private Node quadraticSplit(Transaction tx, Node indexNode, RelationshipType relationshipType) { - // Disconnect all current children from the index and return them with their envelopes - List entries = extractChildNodesWithEnvelopes(indexNode, relationshipType); - - // pick two seed entries such that the dead space is maximal - NodeWithEnvelope[] seeds = mostDistantByDeadSpace(entries); - - List group1 = new ArrayList<>(); - group1.add(seeds[0]); - Envelope group1envelope = seeds[0].envelope; - - List group2 = new ArrayList<>(); - group2.add(seeds[1]); - Envelope group2envelope = seeds[1].envelope; - - entries.remove(seeds[0]); - entries.remove(seeds[1]); - while (entries.size() > 0) { - // compute the cost of inserting each entry - List bestGroup = null; - Envelope bestGroupEnvelope = null; - NodeWithEnvelope bestEntry = null; - double expansionMin = Double.POSITIVE_INFINITY; - for (NodeWithEnvelope e : entries) { - double expansion1 = getArea(createEnvelope(e.envelope, group1envelope)) - getArea(group1envelope); - double expansion2 = getArea(createEnvelope(e.envelope, group2envelope)) - getArea(group2envelope); - - if (expansion1 < expansion2 && expansion1 < expansionMin) { - bestGroup = group1; - bestGroupEnvelope = group1envelope; - bestEntry = e; - expansionMin = expansion1; - } else if (expansion2 < expansion1 && expansion2 < expansionMin) { - bestGroup = group2; - bestGroupEnvelope = group2envelope; - bestEntry = e; - expansionMin = expansion2; - } else if (expansion1 == expansion2 && expansion1 < expansionMin) { - // in case of equality choose the group with the smallest area - if (getArea(group1envelope) < getArea(group2envelope)) { - bestGroup = group1; - bestGroupEnvelope = group1envelope; - } else { - bestGroup = group2; - bestGroupEnvelope = group2envelope; - } - bestEntry = e; - expansionMin = expansion1; - } - } - - if (bestEntry == null) { - throw new RuntimeException("Should not be possible to fail to find a best entry during quadratic split"); - } else { - // insert the best candidate entry in the best group - bestGroup.add(bestEntry); - bestGroupEnvelope.expandToInclude(bestEntry.envelope); - - entries.remove(bestEntry); - } - } - - return reconnectTwoChildGroups(tx, indexNode, group1, group2, relationshipType); - } - - private Node reconnectTwoChildGroups(Transaction tx, Node indexNode, List group1, List group2, RelationshipType relationshipType) { - // reset bounding box and add new children - indexNode.removeProperty(INDEX_PROP_BBOX); - for (NodeWithEnvelope entry : group1) { - addChild(indexNode, relationshipType, entry.node); - } - - // create new node from split - Node newIndexNode = tx.createNode(); - for (NodeWithEnvelope entry : group2) { - addChild(newIndexNode, relationshipType, entry.node); - } - - return newIndexNode; - } - - private void createNewRoot(Transaction tx, Node oldRoot, Node newIndexNode) { - Node newRoot = tx.createNode(); - addChild(newRoot, RTreeRelationshipTypes.RTREE_CHILD, oldRoot); - addChild(newRoot, RTreeRelationshipTypes.RTREE_CHILD, newIndexNode); - - Node layerNode = getRootNode(tx); - layerNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.OUTGOING).delete(); - layerNode.createRelationshipTo(newRoot, RTreeRelationshipTypes.RTREE_ROOT); - } - - private boolean addChild(Node parent, RelationshipType type, Node newChild) { - Envelope childEnvelope = getChildNodeEnvelope(newChild, type); - double[] childBBox = new double[]{ - childEnvelope.getMinX(), childEnvelope.getMinY(), - childEnvelope.getMaxX(), childEnvelope.getMaxY()}; - parent.createRelationshipTo(newChild, type); - return expandParentBoundingBoxAfterNewChild(parent, childBBox); - } - - private void adjustPathBoundingBox(Node node) { - Node parent = getIndexNodeParent(node); - if (parent != null) { - if (adjustParentBoundingBox(parent, RTreeRelationshipTypes.RTREE_CHILD)) { - // entry has been modified: adjust the path for the parent - adjustPathBoundingBox(parent); - } - } - } - - /** - * Fix an IndexNode bounding box after a child has been added or removed removed. Return true if something was - * changed so that parents can also be adjusted. - */ - private boolean adjustParentBoundingBox(Node indexNode, RelationshipType relationshipType) { - double[] old = null; - if (indexNode.hasProperty(INDEX_PROP_BBOX)) { - old = (double[]) indexNode.getProperty(INDEX_PROP_BBOX); - } - - Envelope bbox = null; - - try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { - for (Relationship relationship : relationships) { - Node childNode = relationship.getEndNode(); - - if (bbox == null) { - bbox = new Envelope(getChildNodeEnvelope(childNode, relationshipType)); - } else { - bbox.expandToInclude(getChildNodeEnvelope(childNode, relationshipType)); - } - } - } - - if (bbox == null) { - // this could happen in an empty tree - bbox = new Envelope(0, 0, 0, 0); - } - - if (old == null || old.length != 4 - || bbox.getMinX() != old[0] - || bbox.getMinY() != old[1] - || bbox.getMaxX() != old[2] - || bbox.getMaxY() != old[3]) { - setIndexNodeEnvelope(indexNode, bbox); - return true; - } else { - return false; - } - } - - protected void setIndexNodeEnvelope(Node indexNode, Envelope bbox) { - indexNode.setProperty(INDEX_PROP_BBOX, new double[]{bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY()}); - } - - /** - * Adjust IndexNode bounding box according to the new child inserted - * - * @param parent IndexNode - * @param childBBox geomNode inserted - * @return is bbox changed? - */ - protected boolean expandParentBoundingBoxAfterNewChild(Node parent, double[] childBBox) { - if (!parent.hasProperty(INDEX_PROP_BBOX)) { - parent.setProperty(INDEX_PROP_BBOX, new double[]{childBBox[0], childBBox[1], childBBox[2], childBBox[3]}); - return true; - } - - double[] parentBBox = (double[]) parent.getProperty(INDEX_PROP_BBOX); - - boolean valueChanged = setMin(parentBBox, childBBox, 0); - valueChanged = setMin(parentBBox, childBBox, 1) || valueChanged; - valueChanged = setMax(parentBBox, childBBox, 2) || valueChanged; - valueChanged = setMax(parentBBox, childBBox, 3) || valueChanged; - - if (valueChanged) { - parent.setProperty(INDEX_PROP_BBOX, parentBBox); - } - - return valueChanged; - } - - private boolean setMin(double[] parent, double[] child, int index) { - if (parent[index] > child[index]) { - parent[index] = child[index]; - return true; - } else { - return false; - } - } - - private boolean setMax(double[] parent, double[] child, int index) { - if (parent[index] < child[index]) { - parent[index] = child[index]; - return true; - } else { - return false; - } - } - - private Node getIndexNodeParent(Node indexNode) { - Relationship relationship = indexNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, Direction.INCOMING); - if (relationship == null) { - return null; - } else { - return relationship.getStartNode(); - } - } - - private double getArea(Envelope e) { - return e.getArea(); - } - - private void deleteTreeBelow(Transaction ignored, Node rootNode) { - try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship relationship : relationships) { - deleteRecursivelySubtree(relationship.getEndNode(), relationship); - } - } - } - - private void deleteRecursivelySubtree(Node node, Relationship incoming) { - try (var relationships = node.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - for (Relationship relationship : relationships) { - deleteRecursivelySubtree(relationship.getEndNode(), relationship); - } - } - if (incoming != null) { - incoming.delete(); - } - try (var relationships = node.getRelationships()) { - for (Relationship rel : relationships) { - System.out.println("Unexpected relationship found on " + node + ": " + rel.toString()); - rel.delete(); - } - } - node.delete(); - } - - protected boolean isGeometryNodeIndexed(Node geomNode) { - return geomNode.hasRelationship(Direction.INCOMING, RTreeRelationshipTypes.RTREE_REFERENCE); - } - - protected Node findLeafContainingGeometryNode(Node geomNode) { - return geomNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING).getStartNode(); - } - - protected boolean isIndexNodeInThisIndex(Transaction tx, Node indexNode) { - Node child = indexNode; - Node root = null; - while (root == null) { - Node parent = getIndexNodeParent(child); - if (parent == null) { - root = child; - } else { - child = parent; - } - } - return root.getElementId().equals(getIndexRoot(tx).getElementId()); - } - - private void deleteNode(Node node) { - try (var relationships = node.getRelationships()) { - for (Relationship r : relationships) { - r.delete(); - } - } - node.delete(); - } - - private Node getRootNode(Transaction tx) { - return tx.getNodeByElementId(rootNodeId); - } - - /** - * Create a bounding box encompassing the two bounding boxes passed in. - */ - private static Envelope createEnvelope(Envelope e, Envelope e1) { - Envelope result = new Envelope(e); - result.expandToInclude(e1); - return result; - } - - // Private classes - private static class WarmUpVisitor implements SpatialIndexVisitor { - - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return true; - } - - public void onIndexReference(Node geomNode) { - } - } - - /** - * In order to wrap one iterable or iterator in another that converts - * the objects from one type to another without loading all into memory, - * we need to use this ugly java-magic. Man, I miss Ruby right now! - */ - private static class IndexNodeToGeometryNodeIterable implements Iterable { - - private final Iterator allIndexNodeIterator; - - private class GeometryNodeIterator implements Iterator { - - Iterator geometryNodeIterator = null; - - public boolean hasNext() { - checkGeometryNodeIterator(); - return geometryNodeIterator != null && geometryNodeIterator.hasNext(); - } - - public Node next() { - checkGeometryNodeIterator(); - return geometryNodeIterator == null ? null : geometryNodeIterator.next(); - } - - private void checkGeometryNodeIterator() { - MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); - TraversalDescription td = traversal - .depthFirst() - .relationships(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.OUTGOING) - .evaluator(Evaluators.excludeStartPosition()) - .evaluator(Evaluators.toDepth(1)); - while ((geometryNodeIterator == null || !geometryNodeIterator.hasNext()) && - allIndexNodeIterator.hasNext()) { - geometryNodeIterator = td.traverse(allIndexNodeIterator.next()).nodes().iterator(); - } - } - - public void remove() { - } - } - - public IndexNodeToGeometryNodeIterable(Iterable allIndexNodes) { - this.allIndexNodeIterator = allIndexNodes.iterator(); - } - - public Iterator iterator() { - return new GeometryNodeIterator(); - } - } - - private static class IndexNodeAreaComparator implements Comparator { - - @Override - public int compare(NodeWithEnvelope o1, NodeWithEnvelope o2) { - return Double.compare(o1.envelope.getArea(), o2.envelope.getArea()); - } - } + if (parent == null) { + // if indexNode is the root + createNewRoot(tx, indexNode, newIndexNode); + } else { + expandParentBoundingBoxAfterNewChild(parent, (double[]) indexNode.getProperty(INDEX_PROP_BBOX)); + + addChild(parent, RTreeRelationshipTypes.RTREE_CHILD, newIndexNode); + + if (countChildren(parent, RTreeRelationshipTypes.RTREE_CHILD) > maxNodeReferences) { + splitAndAdjustPathBoundingBox(tx, parent); + } else { + adjustPathBoundingBox(parent); + } + } + monitor.addSplit(newIndexNode); + } + + private Node quadraticSplit(Transaction tx, Node indexNode) { + if (nodeIsLeaf(indexNode)) { + return quadraticSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); + } else { + return quadraticSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_CHILD); + } + } + + private Node greenesSplit(Transaction tx, Node indexNode) { + if (nodeIsLeaf(indexNode)) { + return greenesSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_REFERENCE); + } else { + return greenesSplit(tx, indexNode, RTreeRelationshipTypes.RTREE_CHILD); + } + } + + private NodeWithEnvelope[] mostDistantByDeadSpace(List entries) { + NodeWithEnvelope seed1 = entries.get(0); + NodeWithEnvelope seed2 = entries.get(0); + double worst = Double.NEGATIVE_INFINITY; + for (int i = 0; i < entries.size(); ++i) { + NodeWithEnvelope e = entries.get(i); + for (int j = i + 1; j < entries.size(); ++j) { + NodeWithEnvelope e1 = entries.get(j); + double deadSpace = e.envelope.separation(e1.envelope); + if (deadSpace > worst) { + worst = deadSpace; + seed1 = e; + seed2 = e1; + } + } + } + return new NodeWithEnvelope[]{seed1, seed2}; + } + + private int findLongestDimension(List entries) { + if (entries.size() > 0) { + Envelope env = new Envelope(entries.get(0).envelope); + for (NodeWithEnvelope entry : entries) { + env.expandToInclude(entry.envelope); + } + int longestDimension = 0; + double maxWidth = Double.NEGATIVE_INFINITY; + for (int i = 0; i < env.getDimension(); i++) { + double width = env.getWidth(i); + if (width > maxWidth) { + maxWidth = width; + longestDimension = i; + } + } + return longestDimension; + } else { + return 0; + } + } + + private List extractChildNodesWithEnvelopes(Node indexNode, RelationshipType relationshipType) { + List entries = new ArrayList<>(); + + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { + for (Relationship relationship : relationships) { + Node node = relationship.getEndNode(); + entries.add(new NodeWithEnvelope(node, getChildNodeEnvelope(node, relationshipType))); + relationship.delete(); + } + } + return entries; + } + + private Node greenesSplit(Transaction tx, Node indexNode, RelationshipType relationshipType) { + // Disconnect all current children from the index and return them with their envelopes + List entries = extractChildNodesWithEnvelopes(indexNode, relationshipType); + + // We want to split by the longest dimension to avoid degrading into extremely thin envelopes + int longestDimension = findLongestDimension(entries); + + // Sort the entries by the longest dimension and then create envelopes around left and right halves + entries.sort(new SingleDimensionNodeEnvelopeComparator(longestDimension)); + int splitAt = entries.size() / 2; + List left = entries.subList(0, splitAt); + List right = entries.subList(splitAt, entries.size()); + + return reconnectTwoChildGroups(tx, indexNode, left, right, relationshipType); + } + + private static class SingleDimensionNodeEnvelopeComparator implements Comparator { + + private final int dimension; + + public SingleDimensionNodeEnvelopeComparator(int dimension) { + this.dimension = dimension; + } + + @Override + public int compare(NodeWithEnvelope o1, NodeWithEnvelope o2) { + double length = o2.envelope.centre(dimension) - o1.envelope.centre(dimension); + return Double.compare(length, 0.0); + } + } + + private Node quadraticSplit(Transaction tx, Node indexNode, RelationshipType relationshipType) { + // Disconnect all current children from the index and return them with their envelopes + List entries = extractChildNodesWithEnvelopes(indexNode, relationshipType); + + // pick two seed entries such that the dead space is maximal + NodeWithEnvelope[] seeds = mostDistantByDeadSpace(entries); + + List group1 = new ArrayList<>(); + group1.add(seeds[0]); + Envelope group1envelope = seeds[0].envelope; + + List group2 = new ArrayList<>(); + group2.add(seeds[1]); + Envelope group2envelope = seeds[1].envelope; + + entries.remove(seeds[0]); + entries.remove(seeds[1]); + while (entries.size() > 0) { + // compute the cost of inserting each entry + List bestGroup = null; + Envelope bestGroupEnvelope = null; + NodeWithEnvelope bestEntry = null; + double expansionMin = Double.POSITIVE_INFINITY; + for (NodeWithEnvelope e : entries) { + double expansion1 = getArea(createEnvelope(e.envelope, group1envelope)) - getArea(group1envelope); + double expansion2 = getArea(createEnvelope(e.envelope, group2envelope)) - getArea(group2envelope); + + if (expansion1 < expansion2 && expansion1 < expansionMin) { + bestGroup = group1; + bestGroupEnvelope = group1envelope; + bestEntry = e; + expansionMin = expansion1; + } else if (expansion2 < expansion1 && expansion2 < expansionMin) { + bestGroup = group2; + bestGroupEnvelope = group2envelope; + bestEntry = e; + expansionMin = expansion2; + } else if (expansion1 == expansion2 && expansion1 < expansionMin) { + // in case of equality choose the group with the smallest area + if (getArea(group1envelope) < getArea(group2envelope)) { + bestGroup = group1; + bestGroupEnvelope = group1envelope; + } else { + bestGroup = group2; + bestGroupEnvelope = group2envelope; + } + bestEntry = e; + expansionMin = expansion1; + } + } + + if (bestEntry == null) { + throw new RuntimeException( + "Should not be possible to fail to find a best entry during quadratic split"); + } else { + // insert the best candidate entry in the best group + bestGroup.add(bestEntry); + bestGroupEnvelope.expandToInclude(bestEntry.envelope); + + entries.remove(bestEntry); + } + } + + return reconnectTwoChildGroups(tx, indexNode, group1, group2, relationshipType); + } + + private Node reconnectTwoChildGroups(Transaction tx, Node indexNode, List group1, + List group2, RelationshipType relationshipType) { + // reset bounding box and add new children + indexNode.removeProperty(INDEX_PROP_BBOX); + for (NodeWithEnvelope entry : group1) { + addChild(indexNode, relationshipType, entry.node); + } + + // create new node from split + Node newIndexNode = tx.createNode(); + for (NodeWithEnvelope entry : group2) { + addChild(newIndexNode, relationshipType, entry.node); + } + + return newIndexNode; + } + + private void createNewRoot(Transaction tx, Node oldRoot, Node newIndexNode) { + Node newRoot = tx.createNode(); + addChild(newRoot, RTreeRelationshipTypes.RTREE_CHILD, oldRoot); + addChild(newRoot, RTreeRelationshipTypes.RTREE_CHILD, newIndexNode); + + Node layerNode = getRootNode(tx); + layerNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_ROOT, Direction.OUTGOING).delete(); + layerNode.createRelationshipTo(newRoot, RTreeRelationshipTypes.RTREE_ROOT); + } + + private boolean addChild(Node parent, RelationshipType type, Node newChild) { + Envelope childEnvelope = getChildNodeEnvelope(newChild, type); + double[] childBBox = new double[]{ + childEnvelope.getMinX(), childEnvelope.getMinY(), + childEnvelope.getMaxX(), childEnvelope.getMaxY()}; + parent.createRelationshipTo(newChild, type); + return expandParentBoundingBoxAfterNewChild(parent, childBBox); + } + + private void adjustPathBoundingBox(Node node) { + Node parent = getIndexNodeParent(node); + if (parent != null) { + if (adjustParentBoundingBox(parent, RTreeRelationshipTypes.RTREE_CHILD)) { + // entry has been modified: adjust the path for the parent + adjustPathBoundingBox(parent); + } + } + } + + /** + * Fix an IndexNode bounding box after a child has been added or removed removed. Return true if something was + * changed so that parents can also be adjusted. + */ + private boolean adjustParentBoundingBox(Node indexNode, RelationshipType relationshipType) { + double[] old = null; + if (indexNode.hasProperty(INDEX_PROP_BBOX)) { + old = (double[]) indexNode.getProperty(INDEX_PROP_BBOX); + } + + Envelope bbox = null; + + try (var relationships = indexNode.getRelationships(Direction.OUTGOING, relationshipType)) { + for (Relationship relationship : relationships) { + Node childNode = relationship.getEndNode(); + + if (bbox == null) { + bbox = new Envelope(getChildNodeEnvelope(childNode, relationshipType)); + } else { + bbox.expandToInclude(getChildNodeEnvelope(childNode, relationshipType)); + } + } + } + + if (bbox == null) { + // this could happen in an empty tree + bbox = new Envelope(0, 0, 0, 0); + } + + if (old == null || old.length != 4 + || bbox.getMinX() != old[0] + || bbox.getMinY() != old[1] + || bbox.getMaxX() != old[2] + || bbox.getMaxY() != old[3]) { + setIndexNodeEnvelope(indexNode, bbox); + return true; + } else { + return false; + } + } + + protected void setIndexNodeEnvelope(Node indexNode, Envelope bbox) { + indexNode.setProperty(INDEX_PROP_BBOX, + new double[]{bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY()}); + } + + /** + * Adjust IndexNode bounding box according to the new child inserted + * + * @param parent IndexNode + * @param childBBox geomNode inserted + * @return is bbox changed? + */ + protected boolean expandParentBoundingBoxAfterNewChild(Node parent, double[] childBBox) { + if (!parent.hasProperty(INDEX_PROP_BBOX)) { + parent.setProperty(INDEX_PROP_BBOX, new double[]{childBBox[0], childBBox[1], childBBox[2], childBBox[3]}); + return true; + } + + double[] parentBBox = (double[]) parent.getProperty(INDEX_PROP_BBOX); + + boolean valueChanged = setMin(parentBBox, childBBox, 0); + valueChanged = setMin(parentBBox, childBBox, 1) || valueChanged; + valueChanged = setMax(parentBBox, childBBox, 2) || valueChanged; + valueChanged = setMax(parentBBox, childBBox, 3) || valueChanged; + + if (valueChanged) { + parent.setProperty(INDEX_PROP_BBOX, parentBBox); + } + + return valueChanged; + } + + private boolean setMin(double[] parent, double[] child, int index) { + if (parent[index] > child[index]) { + parent[index] = child[index]; + return true; + } else { + return false; + } + } + + private boolean setMax(double[] parent, double[] child, int index) { + if (parent[index] < child[index]) { + parent[index] = child[index]; + return true; + } else { + return false; + } + } + + private Node getIndexNodeParent(Node indexNode) { + Relationship relationship = indexNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_CHILD, + Direction.INCOMING); + if (relationship == null) { + return null; + } else { + return relationship.getStartNode(); + } + } + + private double getArea(Envelope e) { + return e.getArea(); + } + + private void deleteTreeBelow(Transaction ignored, Node rootNode) { + try (var relationships = rootNode.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship relationship : relationships) { + deleteRecursivelySubtree(relationship.getEndNode(), relationship); + } + } + } + + private void deleteRecursivelySubtree(Node node, Relationship incoming) { + try (var relationships = node.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + for (Relationship relationship : relationships) { + deleteRecursivelySubtree(relationship.getEndNode(), relationship); + } + } + if (incoming != null) { + incoming.delete(); + } + try (var relationships = node.getRelationships()) { + for (Relationship rel : relationships) { + System.out.println("Unexpected relationship found on " + node + ": " + rel.toString()); + rel.delete(); + } + } + node.delete(); + } + + protected boolean isGeometryNodeIndexed(Node geomNode) { + return geomNode.hasRelationship(Direction.INCOMING, RTreeRelationshipTypes.RTREE_REFERENCE); + } + + protected Node findLeafContainingGeometryNode(Node geomNode) { + return geomNode.getSingleRelationship(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.INCOMING) + .getStartNode(); + } + + protected boolean isIndexNodeInThisIndex(Transaction tx, Node indexNode) { + Node child = indexNode; + Node root = null; + while (root == null) { + Node parent = getIndexNodeParent(child); + if (parent == null) { + root = child; + } else { + child = parent; + } + } + return root.getElementId().equals(getIndexRoot(tx).getElementId()); + } + + private void deleteNode(Node node) { + try (var relationships = node.getRelationships()) { + for (Relationship r : relationships) { + r.delete(); + } + } + node.delete(); + } + + private Node getRootNode(Transaction tx) { + return tx.getNodeByElementId(rootNodeId); + } + + /** + * Create a bounding box encompassing the two bounding boxes passed in. + */ + private static Envelope createEnvelope(Envelope e, Envelope e1) { + Envelope result = new Envelope(e); + result.expandToInclude(e1); + return result; + } + + // Private classes + private static class WarmUpVisitor implements SpatialIndexVisitor { + + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return true; + } + + public void onIndexReference(Node geomNode) { + } + } + + /** + * In order to wrap one iterable or iterator in another that converts + * the objects from one type to another without loading all into memory, + * we need to use this ugly java-magic. Man, I miss Ruby right now! + */ + private static class IndexNodeToGeometryNodeIterable implements Iterable { + + private final Iterator allIndexNodeIterator; + + private class GeometryNodeIterator implements Iterator { + + Iterator geometryNodeIterator = null; + + public boolean hasNext() { + checkGeometryNodeIterator(); + return geometryNodeIterator != null && geometryNodeIterator.hasNext(); + } + + public Node next() { + checkGeometryNodeIterator(); + return geometryNodeIterator == null ? null : geometryNodeIterator.next(); + } + + private void checkGeometryNodeIterator() { + MonoDirectionalTraversalDescription traversal = new MonoDirectionalTraversalDescription(); + TraversalDescription td = traversal + .depthFirst() + .relationships(RTreeRelationshipTypes.RTREE_REFERENCE, Direction.OUTGOING) + .evaluator(Evaluators.excludeStartPosition()) + .evaluator(Evaluators.toDepth(1)); + while ((geometryNodeIterator == null || !geometryNodeIterator.hasNext()) && + allIndexNodeIterator.hasNext()) { + geometryNodeIterator = td.traverse(allIndexNodeIterator.next()).nodes().iterator(); + } + } + + public void remove() { + } + } + + public IndexNodeToGeometryNodeIterable(Iterable allIndexNodes) { + this.allIndexNodeIterator = allIndexNodes.iterator(); + } + + public Iterator iterator() { + return new GeometryNodeIterator(); + } + } + + private static class IndexNodeAreaComparator implements Comparator { + + @Override + public int compare(NodeWithEnvelope o1, NodeWithEnvelope o2) { + return Double.compare(o1.envelope.getArea(), o2.envelope.getArea()); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeMonitor.java b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeMonitor.java index 557b6a890..e2e25be2e 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeMonitor.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeMonitor.java @@ -20,105 +20,105 @@ package org.neo4j.gis.spatial.rtree; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Transaction; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; public class RTreeMonitor implements TreeMonitor { - private int nbrSplit; - private int height; - private int nbrRebuilt; - private HashMap cases = new HashMap<>(); - private ArrayList> matchedTreeNodes = new ArrayList<>(); - - public RTreeMonitor() { - reset(); - } - - @Override - public void setHeight(int height) { - this.height = height; - } - - public int getHeight() { - return height; - } - - @Override - public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) { - nbrRebuilt++; - } - - @Override - public int getNbrRebuilt() { - return nbrRebuilt; - } - - @Override - public void addSplit(Node indexNode) { - nbrSplit++; - } - - @Override - public void beforeMergeTree(Node indexNode, List right) { - - } - - @Override - public void afterMergeTree(Node indexNode) { - - } - - @Override - public int getNbrSplit() { - return nbrSplit; - } - - @Override - public void addCase(String key) { - Integer n = cases.get(key); - if (n != null) { - n++; - } else { - n = 1; - } - cases.put(key, n); - } - - @Override - public Map getCaseCounts() { - return cases; - } - - @Override - public void reset() { - cases.clear(); - height = 0; - nbrRebuilt = 0; - nbrSplit = 0; - matchedTreeNodes.clear(); - } - - @Override - public void matchedTreeNode(int level, Node node) { - ensureMatchedTreeNodeLevel(level); - matchedTreeNodes.get(level).add(node); - } - - private void ensureMatchedTreeNodeLevel(int level) { - while (matchedTreeNodes.size() <= level) { - matchedTreeNodes.add(new ArrayList()); - } - } - - @Override - public List getMatchedTreeNodes(int level) { - ensureMatchedTreeNodeLevel(level); - return matchedTreeNodes.get(level).stream().collect(Collectors.toList()); - } + + private int nbrSplit; + private int height; + private int nbrRebuilt; + private HashMap cases = new HashMap<>(); + private ArrayList> matchedTreeNodes = new ArrayList<>(); + + public RTreeMonitor() { + reset(); + } + + @Override + public void setHeight(int height) { + this.height = height; + } + + public int getHeight() { + return height; + } + + @Override + public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) { + nbrRebuilt++; + } + + @Override + public int getNbrRebuilt() { + return nbrRebuilt; + } + + @Override + public void addSplit(Node indexNode) { + nbrSplit++; + } + + @Override + public void beforeMergeTree(Node indexNode, List right) { + + } + + @Override + public void afterMergeTree(Node indexNode) { + + } + + @Override + public int getNbrSplit() { + return nbrSplit; + } + + @Override + public void addCase(String key) { + Integer n = cases.get(key); + if (n != null) { + n++; + } else { + n = 1; + } + cases.put(key, n); + } + + @Override + public Map getCaseCounts() { + return cases; + } + + @Override + public void reset() { + cases.clear(); + height = 0; + nbrRebuilt = 0; + nbrSplit = 0; + matchedTreeNodes.clear(); + } + + @Override + public void matchedTreeNode(int level, Node node) { + ensureMatchedTreeNodeLevel(level); + matchedTreeNodes.get(level).add(node); + } + + private void ensureMatchedTreeNodeLevel(int level) { + while (matchedTreeNodes.size() <= level) { + matchedTreeNodes.add(new ArrayList()); + } + } + + @Override + public List getMatchedTreeNodes(int level) { + ensureMatchedTreeNodeLevel(level); + return matchedTreeNodes.get(level).stream().collect(Collectors.toList()); + } } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeRelationshipTypes.java b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeRelationshipTypes.java index 894570f0f..a89f3f58e 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/RTreeRelationshipTypes.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/RTreeRelationshipTypes.java @@ -23,10 +23,10 @@ public enum RTreeRelationshipTypes implements RelationshipType { - - RTREE_METADATA, - RTREE_ROOT, - RTREE_CHILD, + + RTREE_METADATA, + RTREE_ROOT, + RTREE_CHILD, RTREE_REFERENCE - -} \ No newline at end of file + +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexRecordCounter.java b/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexRecordCounter.java index 7ac8d76e1..cb31734c7 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexRecordCounter.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexRecordCounter.java @@ -25,19 +25,19 @@ public class SpatialIndexRecordCounter implements SpatialIndexVisitor { @Override - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return true; - } - + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return true; + } + @Override - public void onIndexReference(Node geomNode) { - count++; + public void onIndexReference(Node geomNode) { + count++; } - - public int getResult() { - return count; + + public int getResult() { + return count; } - - + + private int count = 0; -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexVisitor.java b/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexVisitor.java index f5c988136..378c462f1 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexVisitor.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/SpatialIndexVisitor.java @@ -27,5 +27,5 @@ public interface SpatialIndexVisitor { boolean needsToVisit(Envelope indexNodeEnvelope); void onIndexReference(Node geomNode); - -} \ No newline at end of file + +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/TreeMonitor.java b/src/main/java/org/neo4j/gis/spatial/rtree/TreeMonitor.java index 914fc8768..5e190a2d2 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/TreeMonitor.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/TreeMonitor.java @@ -21,37 +21,36 @@ package org.neo4j.gis.spatial.rtree; +import java.util.List; +import java.util.Map; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; -import java.util.List; -import java.util.Map; +public interface TreeMonitor { -public interface TreeMonitor -{ - void setHeight(int height); + void setHeight(int height); - int getHeight(); + int getHeight(); - void addNbrRebuilt(RTreeIndex rtree, Transaction tx); + void addNbrRebuilt(RTreeIndex rtree, Transaction tx); - int getNbrRebuilt(); + int getNbrRebuilt(); - void addSplit(Node indexNode); + void addSplit(Node indexNode); - void beforeMergeTree(Node indexNode, List right); + void beforeMergeTree(Node indexNode, List right); - void afterMergeTree(Node indexNode); + void afterMergeTree(Node indexNode); - int getNbrSplit(); + int getNbrSplit(); - void addCase(String key); + void addCase(String key); - Map getCaseCounts(); + Map getCaseCounts(); - void reset(); + void reset(); - void matchedTreeNode(int level, Node node); + void matchedTreeNode(int level, Node node); - List getMatchedTreeNodes(int level); + List getMatchedTreeNodes(int level); } diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/filter/AbstractSearchEnvelopeIntersection.java b/src/main/java/org/neo4j/gis/spatial/rtree/filter/AbstractSearchEnvelopeIntersection.java index e5a4a08a0..c588e9f15 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/filter/AbstractSearchEnvelopeIntersection.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/filter/AbstractSearchEnvelopeIntersection.java @@ -25,7 +25,7 @@ import org.neo4j.graphdb.Transaction; public abstract class AbstractSearchEnvelopeIntersection implements SearchFilter { - + protected EnvelopeDecoder decoder; protected Envelope referenceEnvelope; @@ -42,21 +42,21 @@ public Envelope getReferenceEnvelope() { public boolean needsToVisit(Envelope indexNodeEnvelope) { return indexNodeEnvelope.intersects(referenceEnvelope); } - + @Override public final boolean geometryMatches(Transaction tx, Node geomNode) { Envelope geomEnvelope = decoder.decodeEnvelope(geomNode); if (geomEnvelope.intersects(referenceEnvelope)) { return onEnvelopeIntersection(geomNode, geomEnvelope); } - + return false; } - + @Override public String toString() { return "SearchEnvelopeIntersection[" + referenceEnvelope + "]"; } - + protected abstract boolean onEnvelopeIntersection(Node geomNode, Envelope geomEnvelope); -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchAll.java b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchAll.java index 5d1993c8a..d93526e61 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchAll.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchAll.java @@ -25,14 +25,14 @@ public class SearchAll implements SearchFilter { - @Override - public boolean needsToVisit(Envelope indexNodeEnvelope) { - return true; - } + @Override + public boolean needsToVisit(Envelope indexNodeEnvelope) { + return true; + } - @Override - public boolean geometryMatches(Transaction tx, Node geomNode) { - return true; - } + @Override + public boolean geometryMatches(Transaction tx, Node geomNode) { + return true; + } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchCoveredByEnvelope.java b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchCoveredByEnvelope.java index 4e6230ac2..2eb6521e6 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchCoveredByEnvelope.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchCoveredByEnvelope.java @@ -35,7 +35,7 @@ public SearchCoveredByEnvelope(EnvelopeDecoder decoder, Envelope referenceEnvelo @Override protected boolean onEnvelopeIntersection(Node geomNode, Envelope geomEnvelope) { // check if every point of this Envelope is a point of the Reference Envelope - return referenceEnvelope.contains(geomEnvelope); + return referenceEnvelope.contains(geomEnvelope); } -} \ No newline at end of file +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchFilter.java b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchFilter.java index 7a3e582b8..657d4193e 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchFilter.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchFilter.java @@ -24,9 +24,9 @@ import org.neo4j.graphdb.Transaction; public interface SearchFilter { - + boolean needsToVisit(Envelope envelope); - + boolean geometryMatches(Transaction tx, Node geomNode); - -} \ No newline at end of file + +} diff --git a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchResults.java b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchResults.java index adf3e0f28..79c87c0ab 100644 --- a/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchResults.java +++ b/src/main/java/org/neo4j/gis/spatial/rtree/filter/SearchResults.java @@ -19,31 +19,31 @@ */ package org.neo4j.gis.spatial.rtree.filter; -import org.neo4j.graphdb.Node; - import java.util.Iterator; +import org.neo4j.graphdb.Node; public class SearchResults implements Iterable { - private final Iterable traverser; - private int count = -1; - public SearchResults(Iterable traverser) { - this.traverser = traverser; - } + private final Iterable traverser; + private int count = -1; + + public SearchResults(Iterable traverser) { + this.traverser = traverser; + } - @Override - public Iterator iterator() { - return traverser.iterator(); - } + @Override + public Iterator iterator() { + return traverser.iterator(); + } - public int count() { - if (count < 0) { - count = 0; - for (@SuppressWarnings("unused") - Node node : this) { - count++; - } - } - return count; - } + public int count() { + if (count < 0) { + count = 0; + for (@SuppressWarnings("unused") + Node node : this) { + count++; + } + } + return count; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/utilities/GeotoolsAdapter.java b/src/main/java/org/neo4j/gis/spatial/utilities/GeotoolsAdapter.java index 299f38892..0e113f585 100644 --- a/src/main/java/org/neo4j/gis/spatial/utilities/GeotoolsAdapter.java +++ b/src/main/java/org/neo4j/gis/spatial/utilities/GeotoolsAdapter.java @@ -1,13 +1,13 @@ package org.neo4j.gis.spatial.utilities; +import static org.geotools.referencing.crs.DefaultEngineeringCRS.GENERIC_2D; +import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84; + +import org.geotools.api.referencing.FactoryException; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.referencing.CRS; import org.geotools.referencing.ReferencingFactoryFinder; import org.neo4j.gis.spatial.SpatialDatabaseException; -import org.geotools.api.referencing.FactoryException; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import static org.geotools.referencing.crs.DefaultEngineeringCRS.GENERIC_2D; -import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84; /** * This class provides some basic wrappers around geotools calls. @@ -15,29 +15,30 @@ * Once we have ported to a newer Geotools that supports Java 11, we could either remove this class or re-purpose it. */ public class GeotoolsAdapter { - public static CoordinateReferenceSystem getCRS(String crsText) { - // TODO: upgrade geotools to get around bug with java11 support - try { - if (crsText.startsWith("GEOGCS[\"WGS84(DD)\"")) { - return WGS84; - } else if (crsText.startsWith("LOCAL_CS[\"Generic cartesian 2D\"")) { - return GENERIC_2D; - } else { - System.out.println("Attempting to use geotools to lookup CRS - might fail with Java11: " + crsText); - return ReferencingFactoryFinder.getCRSFactory(null).createFromWKT(crsText); - } - } catch (FactoryException e) { - throw new SpatialDatabaseException(e); - } - } - public static Integer getEPSGCode(CoordinateReferenceSystem crs) { - try { - // TODO: upgrade geotools to avoid Java11 failures on CRS.lookupEpsgCode - return (crs == WGS84) ? Integer.valueOf(4326) : (crs == GENERIC_2D) ? null : CRS.lookupEpsgCode(crs, true); - } catch (FactoryException e) { - System.err.println("Failed to lookup CRS: " + e.getMessage()); - return null; - } - } + public static CoordinateReferenceSystem getCRS(String crsText) { + // TODO: upgrade geotools to get around bug with java11 support + try { + if (crsText.startsWith("GEOGCS[\"WGS84(DD)\"")) { + return WGS84; + } else if (crsText.startsWith("LOCAL_CS[\"Generic cartesian 2D\"")) { + return GENERIC_2D; + } else { + System.out.println("Attempting to use geotools to lookup CRS - might fail with Java11: " + crsText); + return ReferencingFactoryFinder.getCRSFactory(null).createFromWKT(crsText); + } + } catch (FactoryException e) { + throw new SpatialDatabaseException(e); + } + } + + public static Integer getEPSGCode(CoordinateReferenceSystem crs) { + try { + // TODO: upgrade geotools to avoid Java11 failures on CRS.lookupEpsgCode + return (crs == WGS84) ? Integer.valueOf(4326) : (crs == GENERIC_2D) ? null : CRS.lookupEpsgCode(crs, true); + } catch (FactoryException e) { + System.err.println("Failed to lookup CRS: " + e.getMessage()); + return null; + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/utilities/LayerUtilities.java b/src/main/java/org/neo4j/gis/spatial/utilities/LayerUtilities.java index e93d96886..acfd7f2f9 100644 --- a/src/main/java/org/neo4j/gis/spatial/utilities/LayerUtilities.java +++ b/src/main/java/org/neo4j/gis/spatial/utilities/LayerUtilities.java @@ -19,7 +19,10 @@ */ package org.neo4j.gis.spatial.utilities; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.Constants; +import org.neo4j.gis.spatial.GeometryEncoder; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SpatialDatabaseException; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.index.LayerIndexReader; import org.neo4j.gis.spatial.index.LayerRTreeIndex; @@ -31,64 +34,69 @@ */ public class LayerUtilities implements Constants { - /** - * Factory method to construct a layer from an existing layerNode. This will read the layer - * class from the layer node properties and construct the correct class from that. - * - * @return new layer instance from existing layer node - */ - @SuppressWarnings("unchecked") - public static Layer makeLayerFromNode(Transaction tx, IndexManager indexManager, Node layerNode) { - try { - String name = (String) layerNode.getProperty(PROP_LAYER); - if (name == null) { - throw new IllegalArgumentException("Node is not a layer node, it has no " + PROP_LAYER + " property: " + layerNode); - } + /** + * Factory method to construct a layer from an existing layerNode. This will read the layer + * class from the layer node properties and construct the correct class from that. + * + * @return new layer instance from existing layer node + */ + @SuppressWarnings("unchecked") + public static Layer makeLayerFromNode(Transaction tx, IndexManager indexManager, Node layerNode) { + try { + String name = (String) layerNode.getProperty(PROP_LAYER); + if (name == null) { + throw new IllegalArgumentException( + "Node is not a layer node, it has no " + PROP_LAYER + " property: " + layerNode); + } - String className = null; - if (layerNode.hasProperty(PROP_LAYER_CLASS)) { - className = (String) layerNode.getProperty(PROP_LAYER_CLASS); - } + String className = null; + if (layerNode.hasProperty(PROP_LAYER_CLASS)) { + className = (String) layerNode.getProperty(PROP_LAYER_CLASS); + } - Class layerClass = className == null ? Layer.class : (Class) Class.forName(className); - return makeLayerInstance(tx, indexManager, name, layerNode, layerClass); - } catch (Exception e) { - throw new RuntimeException(e); - } - } + Class layerClass = + className == null ? Layer.class : (Class) Class.forName(className); + return makeLayerInstance(tx, indexManager, name, layerNode, layerClass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } - /** - * Factory method to construct a layer with the specified layer class. This can be used when - * creating a layer for the first time. It will also construct the underlying Node in the graph. - * - * @return new Layer instance based on newly created layer Node - */ - public static Layer makeLayerAndNode(Transaction tx, IndexManager indexManager, String name, - Class geometryEncoderClass, - Class layerClass, - Class indexClass) { - try { - if (indexClass == null) { - indexClass = LayerRTreeIndex.class; - } - Node layerNode = tx.createNode(); - layerNode.addLabel(LABEL_LAYER); - layerNode.setProperty(PROP_LAYER, name); - layerNode.setProperty(PROP_CREATIONTIME, System.currentTimeMillis()); - layerNode.setProperty(PROP_GEOMENCODER, geometryEncoderClass.getCanonicalName()); - layerNode.setProperty(PROP_INDEX_CLASS, indexClass.getCanonicalName()); - layerNode.setProperty(PROP_LAYER_CLASS, layerClass.getCanonicalName()); - return makeLayerInstance(tx, indexManager, name, layerNode, layerClass); - } catch (Exception e) { - throw new SpatialDatabaseException(e); - } - } + /** + * Factory method to construct a layer with the specified layer class. This can be used when + * creating a layer for the first time. It will also construct the underlying Node in the graph. + * + * @return new Layer instance based on newly created layer Node + */ + public static Layer makeLayerAndNode(Transaction tx, IndexManager indexManager, String name, + Class geometryEncoderClass, + Class layerClass, + Class indexClass) { + try { + if (indexClass == null) { + indexClass = LayerRTreeIndex.class; + } + Node layerNode = tx.createNode(); + layerNode.addLabel(LABEL_LAYER); + layerNode.setProperty(PROP_LAYER, name); + layerNode.setProperty(PROP_CREATIONTIME, System.currentTimeMillis()); + layerNode.setProperty(PROP_GEOMENCODER, geometryEncoderClass.getCanonicalName()); + layerNode.setProperty(PROP_INDEX_CLASS, indexClass.getCanonicalName()); + layerNode.setProperty(PROP_LAYER_CLASS, layerClass.getCanonicalName()); + return makeLayerInstance(tx, indexManager, name, layerNode, layerClass); + } catch (Exception e) { + throw new SpatialDatabaseException(e); + } + } - private static Layer makeLayerInstance(Transaction tx, IndexManager indexManager, String name, Node layerNode, Class layerClass) throws InstantiationException, IllegalAccessException { - if (layerClass == null) layerClass = Layer.class; - Layer layer = layerClass.newInstance(); - layer.initialize(tx, indexManager, name, layerNode); - return layer; - } + private static Layer makeLayerInstance(Transaction tx, IndexManager indexManager, String name, Node layerNode, + Class layerClass) throws InstantiationException, IllegalAccessException { + if (layerClass == null) { + layerClass = Layer.class; + } + Layer layer = layerClass.newInstance(); + layer.initialize(tx, indexManager, name, layerNode); + return layer; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/utilities/ReferenceNodes.java b/src/main/java/org/neo4j/gis/spatial/utilities/ReferenceNodes.java index 004cf04ca..3d571c13a 100644 --- a/src/main/java/org/neo4j/gis/spatial/utilities/ReferenceNodes.java +++ b/src/main/java/org/neo4j/gis/spatial/utilities/ReferenceNodes.java @@ -35,30 +35,30 @@ * even in read-only queries like 'findLayer', it messed with nested transactions where both might * try create the same node, even if the developer was careful to split read and write aspects of the * code. - * + *

* It is time to stop using a root node. This class will remain only for the purpose of helping * users transition older spatial models away from root nodes. */ public class ReferenceNodes { - public static final Label LABEL_REFERENCE = Label.label("ReferenceNode"); - public static final String PROP_NAME = "name"; + public static final Label LABEL_REFERENCE = Label.label("ReferenceNode"); + public static final String PROP_NAME = "name"; - @Deprecated - public static Node getReferenceNode(Transaction tx, String name) { - throw new IllegalStateException("It is no longer valid to use a root or reference node in the spatial model"); - } + @Deprecated + public static Node getReferenceNode(Transaction tx, String name) { + throw new IllegalStateException("It is no longer valid to use a root or reference node in the spatial model"); + } - public static Node findDeprecatedReferenceNode(Transaction tx, String name) { - return tx.findNode(LABEL_REFERENCE, PROP_NAME, name); - } + public static Node findDeprecatedReferenceNode(Transaction tx, String name) { + return tx.findNode(LABEL_REFERENCE, PROP_NAME, name); + } - /** - * Should be used for tests only. No attempt is made to ensure no duplicates are created. - */ - public static Node createDeprecatedReferenceNode(Transaction tx, String name) { - Node node = tx.createNode(LABEL_REFERENCE); - node.setProperty(PROP_NAME, name); - return node; - } + /** + * Should be used for tests only. No attempt is made to ensure no duplicates are created. + */ + public static Node createDeprecatedReferenceNode(Transaction tx, String name) { + Node node = tx.createNode(LABEL_REFERENCE); + node.setProperty(PROP_NAME, name); + return node; + } } diff --git a/src/main/java/org/neo4j/gis/spatial/utilities/RelationshipTraversal.java b/src/main/java/org/neo4j/gis/spatial/utilities/RelationshipTraversal.java index 9890a64aa..3be930772 100644 --- a/src/main/java/org/neo4j/gis/spatial/utilities/RelationshipTraversal.java +++ b/src/main/java/org/neo4j/gis/spatial/utilities/RelationshipTraversal.java @@ -19,37 +19,40 @@ */ package org.neo4j.gis.spatial.utilities; -import org.neo4j.graphdb.*; - import java.util.Iterator; +import org.neo4j.graphdb.Node; /** * Neo4j 4.3 introduced some bugs around closing RelationshipTraversalCursor. * This class provides alternative implementations of suspicious methods to work around that issue. */ public class RelationshipTraversal { - /** - * Normally just calling iterator.next() once should work, but the bug in Neo4j 4.3 results in leaked cursors - * if this iterator comes from the traversal framework, so this code exhausts the traverser and returns the first - * node found. For cases where many results could be found, this is expensive. Try to use only when one or few results - * are likely. - */ - public static Node getFirstNode(Iterable nodes) { - Node found = null; - for (Node node : nodes) { - if (found == null) found = node; - } - return found; - } - /** - * Some code has facilities for closing resource at a high level, but the underlying resources are only - * Iterators, with no access to the original sources and no way to close the resources properly. - * So to avoid the Neo4j 4.3 bug with leaked RelationshipTraversalCursor, we need to exhaust the iterator. - */ - public static void exhaustIterator(Iterator source) { - while (source.hasNext()) { - source.next(); - } - } + /** + * Normally just calling iterator.next() once should work, but the bug in Neo4j 4.3 results in leaked cursors + * if this iterator comes from the traversal framework, so this code exhausts the traverser and returns the first + * node found. For cases where many results could be found, this is expensive. Try to use only when one or few + * results + * are likely. + */ + public static Node getFirstNode(Iterable nodes) { + Node found = null; + for (Node node : nodes) { + if (found == null) { + found = node; + } + } + return found; + } + + /** + * Some code has facilities for closing resource at a high level, but the underlying resources are only + * Iterators, with no access to the original sources and no way to close the resources properly. + * So to avoid the Neo4j 4.3 bug with leaked RelationshipTraversalCursor, we need to exhaust the iterator. + */ + public static void exhaustIterator(Iterator source) { + while (source.hasNext()) { + source.next(); + } + } } diff --git a/src/main/java/org/neo4j/gis/spatial/utilities/TraverserFactory.java b/src/main/java/org/neo4j/gis/spatial/utilities/TraverserFactory.java index 3b7c0efbd..836cb77da 100644 --- a/src/main/java/org/neo4j/gis/spatial/utilities/TraverserFactory.java +++ b/src/main/java/org/neo4j/gis/spatial/utilities/TraverserFactory.java @@ -20,32 +20,28 @@ package org.neo4j.gis.spatial.utilities; import java.lang.reflect.InvocationTargetException; - import org.neo4j.graphdb.Node; import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.graphdb.traversal.Traverser; public class TraverserFactory { - public static Traverser createTraverserInBackwardsCompatibleWay( TraversalDescription traversalDescription, - Node layerNode ) { - try { - try { - return (Traverser) TraversalDescription.class.getDeclaredMethod( "traverse", - Node.class ).invoke( traversalDescription, layerNode ); - } - catch ( NoSuchMethodException e ) { - return (Traverser) TraversalDescription.class.getDeclaredMethod( "traverse", - Node[].class ).invoke( traversalDescription, new Object[]{new Node[]{layerNode}} ); - } - } - catch ( IllegalAccessException e ) { - throw new IllegalStateException( "You seem to be using an unsupported version of Neo4j.", e ); - } - catch ( InvocationTargetException e ) { - throw new IllegalStateException( "You seem to be using an unsupported version of Neo4j.", e ); - } - catch ( NoSuchMethodException e ) { - throw new IllegalStateException( "You seem to be using an unsupported version of Neo4j.", e ); - } - } -} \ No newline at end of file + + public static Traverser createTraverserInBackwardsCompatibleWay(TraversalDescription traversalDescription, + Node layerNode) { + try { + try { + return (Traverser) TraversalDescription.class.getDeclaredMethod("traverse", + Node.class).invoke(traversalDescription, layerNode); + } catch (NoSuchMethodException e) { + return (Traverser) TraversalDescription.class.getDeclaredMethod("traverse", + Node[].class).invoke(traversalDescription, new Object[]{new Node[]{layerNode}}); + } + } catch (IllegalAccessException e) { + throw new IllegalStateException("You seem to be using an unsupported version of Neo4j.", e); + } catch (InvocationTargetException e) { + throw new IllegalStateException("You seem to be using an unsupported version of Neo4j.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("You seem to be using an unsupported version of Neo4j.", e); + } + } +} diff --git a/src/main/python/test_mapnik.py b/src/main/python/test_mapnik.py index 3ab5b21fb..eb9b4fdb6 100755 --- a/src/main/python/test_mapnik.py +++ b/src/main/python/test_mapnik.py @@ -3,62 +3,61 @@ import mapnik import os -m = mapnik.Map(2000,2000,"+proj=latlong +datum=WGS84") +m = mapnik.Map(2000, 2000, "+proj=latlong +datum=WGS84") m.background = mapnik.Color('grey') -#buidling +# buidling s_building = mapnik.Style() -r_building=mapnik.Rule() +r_building = mapnik.Rule() r_building.symbols.append(mapnik.PolygonSymbolizer(mapnik.Color('brown'))) -#r_building.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)) +# r_building.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)) s_building.rules.append(r_building) -m.append_style('building',s_building) -l_building = mapnik.Layer('building',"+proj=latlong +datum=WGS84") +m.append_style('building', s_building) +l_building = mapnik.Layer('building', "+proj=latlong +datum=WGS84") l_building.datasource = mapnik.Shapefile(file='target/export/building') l_building.styles.append('building') -#natural +# natural s_natural = mapnik.Style() -r_natural=mapnik.Rule() +r_natural = mapnik.Rule() r_natural.symbols.append(mapnik.PolygonSymbolizer(mapnik.Color('green'))) -r_natural.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)) +r_natural.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'), 0.1)) s_natural.rules.append(r_natural) -m.append_style('natural',s_natural) -l_natural = mapnik.Layer('natural',"+proj=latlong +datum=WGS84") +m.append_style('natural', s_natural) +l_natural = mapnik.Layer('natural', "+proj=latlong +datum=WGS84") l_natural.datasource = mapnik.Shapefile(file='target/export/natural-wood') l_natural.styles.append('natural') - -#water +# water s_water = mapnik.Style() -r_water=mapnik.Rule() +r_water = mapnik.Rule() r_water.symbols.append(mapnik.PolygonSymbolizer(mapnik.Color('steelblue'))) -#r_water.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)) +# r_water.symbols.append(mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)) s_water.rules.append(r_water) -m.append_style('water',s_water) -l_water = mapnik.Layer('water',"+proj=latlong +datum=WGS84") +m.append_style('water', s_water) +l_water = mapnik.Layer('water', "+proj=latlong +datum=WGS84") l_water.datasource = mapnik.Shapefile(file='target/export/natural-water') l_water.styles.append('water') -#highways +# highways s_highway = mapnik.Style() -r_highway=mapnik.Rule() +r_highway = mapnik.Rule() road_stroke = mapnik.Stroke() road_stroke.width = 2.0 -#dashed lines -#road_stroke.add_dash(8, 4) -#road_stroke.add_dash(2, 2) -#road_stroke.add_dash(2, 2) +# dashed lines +# road_stroke.add_dash(8, 4) +# road_stroke.add_dash(2, 2) +# road_stroke.add_dash(2, 2) road_stroke.color = mapnik.Color('yellow') road_stroke.line_cap = mapnik.line_cap.ROUND_CAP -#r_highway.symbols.append(mapnik.PolygonSymbolizer(mapnik.Color('yellow'))) +# r_highway.symbols.append(mapnik.PolygonSymbolizer(mapnik.Color('yellow'))) r_highway.symbols.append(mapnik.LineSymbolizer(road_stroke)) s_highway.rules.append(r_highway) -m.append_style('highway',s_highway) -l_highway = mapnik.Layer('highway',"+proj=latlong +datum=WGS84") +m.append_style('highway', s_highway) +l_highway = mapnik.Layer('highway', "+proj=latlong +datum=WGS84") l_highway.datasource = mapnik.Shapefile(file='target/export/highway') l_highway.styles.append('highway') -m.append_style('highway',s_highway) +m.append_style('highway', s_highway) m.layers.append(l_building) m.layers.append(l_highway) @@ -67,5 +66,5 @@ m.zoom_to_box(l_highway.envelope()) img_loc = 'target/export.png' mapnik.render_to_file(m, img_loc, 'png') -mapnik.save_map(m,"target/map.xml") +mapnik.save_map(m, "target/map.xml") os.system('open ' + img_loc) diff --git a/src/site/site.xml b/src/site/site.xml index da1e0708d..b4742eaa8 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -21,32 +21,32 @@ --> - - - ${project.name} - images/logos/logo-dark.png - http://neo4j.org/ - - - - - - - - - - -

- - - - - - - - - ${skinGroupId} - ${skinArtifactId} - ${skinVersion} - + + + ${project.name} + images/logos/logo-dark.png + http://neo4j.org/ + + + + + + + + + + + + + + + + + + + + ${skinGroupId} + ${skinArtifactId} + ${skinVersion} + diff --git a/src/test/java/org/neo4j/doc/tools/AsciiDocGenerator.java b/src/test/java/org/neo4j/doc/tools/AsciiDocGenerator.java index a4dc3a241..44cd84fe2 100644 --- a/src/test/java/org/neo4j/doc/tools/AsciiDocGenerator.java +++ b/src/test/java/org/neo4j/doc/tools/AsciiDocGenerator.java @@ -29,319 +29,261 @@ import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; - import org.neo4j.graphdb.GraphDatabaseService; /** * Generate asciidoc-formatted documentation from HTTP requests and responses. * The status and media type of all responses is checked as well as the * existence of any expected headers. - * + *

* The filename of the resulting ASCIIDOC test file is derived from the title. - * + *

* The title is determined by either a JavaDoc period terminated first title * line, the @Title annotation or the method name, where "_" is replaced by " ". */ -public abstract class AsciiDocGenerator -{ - private static final String DOCUMENTATION_END = "\n...\n"; - private final Logger log = Logger.getLogger( AsciiDocGenerator.class.getName() ); - protected final String title; - protected String section; - protected String description = null; - protected GraphDatabaseService graph; - protected static final String SNIPPET_MARKER = "@@"; - protected Map snippets = new HashMap(); - private static final Map counters = new HashMap(); +public abstract class AsciiDocGenerator { + + private static final String DOCUMENTATION_END = "\n...\n"; + private final Logger log = Logger.getLogger(AsciiDocGenerator.class.getName()); + protected final String title; + protected String section; + protected String description = null; + protected GraphDatabaseService graph; + protected static final String SNIPPET_MARKER = "@@"; + protected Map snippets = new HashMap(); + private static final Map counters = new HashMap(); + + public AsciiDocGenerator(final String title, final String section) { + this.section = section; + this.title = title.replace("_", " "); + } + + public AsciiDocGenerator setGraph(GraphDatabaseService graph) { + this.graph = graph; + return this; + } + + public String getTitle() { + return title; + } + + public AsciiDocGenerator setSection(final String section) { + this.section = section; + return this; + } - public AsciiDocGenerator( final String title, final String section ) - { - this.section = section; - this.title = title.replace( "_", " " ); - } + /** + * Add a description to the test (in asciidoc format). Adding multiple + * descriptions will yield one paragraph per description. + * + * @param description the description + */ + public AsciiDocGenerator description(final String description) { + if (description == null) { + throw new IllegalArgumentException( + "The description can not be null"); + } + String content; + int pos = description.indexOf(DOCUMENTATION_END); + if (pos != -1) { + content = description.substring(0, pos); + } else { + content = description; + } + if (this.description == null) { + this.description = content; + } else { + this.description += "\n\n" + content; + } + return this; + } - public AsciiDocGenerator setGraph( GraphDatabaseService graph ) - { - this.graph = graph; - return this; - } - - public String getTitle() - { - return title; - } - - public AsciiDocGenerator setSection(final String section) - { - this.section = section; - return this; - } + protected void line(final Writer fw, final String string) + throws IOException { + fw.append(string); + fw.append("\n"); + } - /** - * Add a description to the test (in asciidoc format). Adding multiple - * descriptions will yield one paragraph per description. - * - * @param description the description - */ - public AsciiDocGenerator description( final String description ) - { - if ( description == null ) - { - throw new IllegalArgumentException( - "The description can not be null" ); - } - String content; - int pos = description.indexOf( DOCUMENTATION_END ); - if ( pos != -1 ) - { - content = description.substring( 0, pos ); - } - else - { - content = description; - } - if ( this.description == null ) - { - this.description = content; - } - else - { - this.description += "\n\n" + content; - } - return this; - } + public static Writer getFW(String dir, String title) { + try { + File dirs = new File(dir); + String name = title.replace(" ", "-") + .toLowerCase() + ".asciidoc"; + return getFW(dirs, name); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } - protected void line( final Writer fw, final String string ) - throws IOException - { - fw.append( string ); - fw.append( "\n" ); - } + public static Writer getFW(File dir, String filename) { + try { + if (!dir.exists()) { + dir.mkdirs(); + } + File out = new File(dir, filename); + if (out.exists()) { + out.delete(); + } + if (!out.createNewFile()) { + throw new RuntimeException("File exists: " + + out.getAbsolutePath()); + } + return new OutputStreamWriter(new FileOutputStream(out, false), StandardCharsets.UTF_8); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } - public static Writer getFW( String dir, String title ) - { - try - { - File dirs = new File( dir ); - String name = title.replace( " ", "-" ) - .toLowerCase() + ".asciidoc"; - return getFW( dirs, name ); - } - catch ( Exception e ) - { - e.printStackTrace(); - throw new RuntimeException( e ); - } - } + public static String dumpToSeparateFile(File dir, String testId, + String content) { + if (content == null || content.isEmpty()) { + throw new IllegalArgumentException("The content can not be empty(" + content + ")."); + } + String filename = testId + ".asciidoc"; + Writer writer = AsciiDocGenerator.getFW(new File(dir, "includes"), filename); + String title = ""; + char firstChar = content.charAt(0); + if (firstChar == '.' || firstChar == '_') { + int pos = content.indexOf('\n'); + if (pos != -1) { + title = content.substring(0, pos + 1); + content = content.substring(pos + 1); + } + } + try { + writer.write(content); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return title + "include::includes/" + filename + "[]\n"; + } - public static Writer getFW( File dir, String filename ) - { - try - { - if ( !dir.exists() ) - { - dir.mkdirs(); - } - File out = new File( dir, filename ); - if ( out.exists() ) - { - out.delete(); - } - if ( !out.createNewFile() ) - { - throw new RuntimeException( "File exists: " - + out.getAbsolutePath() ); - } - return new OutputStreamWriter( new FileOutputStream( out, false ), StandardCharsets.UTF_8 ); - } - catch ( Exception e ) - { - e.printStackTrace(); - throw new RuntimeException( e ); - } - } - - public static String dumpToSeparateFile( File dir, String testId, - String content ) - { - if ( content == null || content.isEmpty() ) - { - throw new IllegalArgumentException( "The content can not be empty(" + content + ")." ); - } - String filename = testId + ".asciidoc"; - Writer writer = AsciiDocGenerator.getFW( new File( dir, "includes" ), filename ); - String title = ""; - char firstChar = content.charAt( 0 ); - if ( firstChar == '.' || firstChar == '_' ) - { - int pos = content.indexOf( '\n' ); - if ( pos != -1 ) - { - title = content.substring( 0, pos + 1 ); - content = content.substring( pos + 1 ); - } - } - try - { - writer.write( content ); - } - catch ( IOException e ) - { - e.printStackTrace(); - } - finally - { - try - { - writer.close(); - } - catch ( IOException e ) - { - e.printStackTrace(); - } - } - return title + "include::includes/" + filename + "[]\n"; - } + public static String dumpToSeparateFileWithType(File dir, String type, + String content) { + if (type == null || type.isEmpty()) { + throw new IllegalArgumentException( + "The type can not be null or empty: [" + type + "]"); + } + String key = dir.getAbsolutePath() + type; + Integer counter = counters.get(key); + if (counter == null) { + counter = 0; + } + counter++; + counters.put(key, counter); + String testId = type + "-" + String.valueOf(counter); + return dumpToSeparateFile(dir, testId, content); + } - public static String dumpToSeparateFileWithType( File dir, String type, - String content ) - { - if ( type == null || type.isEmpty() ) - { - throw new IllegalArgumentException( - "The type can not be null or empty: [" + type + "]" ); - } - String key = dir.getAbsolutePath() + type; - Integer counter = counters.get( key ); - if ( counter == null ) - { - counter = 0; - } - counter++; - counters.put( key, counter ); - String testId = type + "-" + String.valueOf( counter ); - return dumpToSeparateFile( dir, testId, content ); - } + public static PrintWriter getPrintWriter(String dir, String title) { + return new PrintWriter(getFW(dir, title)); + } - public static PrintWriter getPrintWriter( String dir, String title ) - { - return new PrintWriter( getFW( dir, title ) ); - } - - public static String getPath( Class source ) - { - return source.getPackage() - .getName() - .replace( ".", "/" ) + "/" + source.getSimpleName() + ".java"; - } + public static String getPath(Class source) { + return source.getPackage() + .getName() + .replace(".", "/") + "/" + source.getSimpleName() + ".java"; + } - protected String replaceSnippets( String description, File dir, String title ) - { - for (String key : snippets.keySet()) { - description = replaceSnippet( description, key, dir, title ); - } - if(description.contains( SNIPPET_MARKER )) { - int indexOf = description.indexOf( SNIPPET_MARKER ); - String snippet = description.substring( indexOf, description.indexOf( "\n", indexOf ) ); - log.severe( "missing snippet ["+snippet+"] in " + description); - } - return description; - } + protected String replaceSnippets(String description, File dir, String title) { + for (String key : snippets.keySet()) { + description = replaceSnippet(description, key, dir, title); + } + if (description.contains(SNIPPET_MARKER)) { + int indexOf = description.indexOf(SNIPPET_MARKER); + String snippet = description.substring(indexOf, description.indexOf("\n", indexOf)); + log.severe("missing snippet [" + snippet + "] in " + description); + } + return description; + } - private String replaceSnippet( String description, String key, File dir, - String title ) - { - String snippetString = SNIPPET_MARKER + key; - if ( description.contains( snippetString + "\n" ) ) - { - String include = dumpToSeparateFile( dir, title + "-" + key, - snippets.get( key ) ); - description = description.replace( snippetString + "\n", include ); - } else { - log.severe( "Could not find " + snippetString + "\\n in " - + description ); - } - return description; - } + private String replaceSnippet(String description, String key, File dir, + String title) { + String snippetString = SNIPPET_MARKER + key; + if (description.contains(snippetString + "\n")) { + String include = dumpToSeparateFile(dir, title + "-" + key, + snippets.get(key)); + description = description.replace(snippetString + "\n", include); + } else { + log.severe("Could not find " + snippetString + "\\n in " + + description); + } + return description; + } - /** - * Add snippets that will be replaced into corresponding. - * - * A snippet needs to be on its own line, terminated by "\n". - * - * @@snippetname placeholders in the content of the description. - * - * @param key the snippet key, without @@ - * @param content the content to be inserted - */ - public void addSnippet( String key, String content ) - { - snippets.put( key, content ); - } + /** + * Add snippets that will be replaced into corresponding. + *

+ * A snippet needs to be on its own line, terminated by "\n". + * + * @param key the snippet key, without @@ + * @param content the content to be inserted + * @@snippetname placeholders in the content of the description. + */ + public void addSnippet(String key, String content) { + snippets.put(key, content); + } - /** - * Added one or more source snippets from test sources, available from - * javadoc using - * - * @@tagName. - * - * @param source the class where the snippet is found - * @param tagNames the tag names which should be included - */ - public void addTestSourceSnippets( Class source, String... tagNames ) - { - for ( String tagName : tagNames ) - { - addSnippet( tagName, sourceSnippet( tagName, source, "test-sources" ) ); - } - } + /** + * Added one or more source snippets from test sources, available from + * javadoc using + * + * @param source the class where the snippet is found + * @param tagNames the tag names which should be included + * @@tagName. + */ + public void addTestSourceSnippets(Class source, String... tagNames) { + for (String tagName : tagNames) { + addSnippet(tagName, sourceSnippet(tagName, source, "test-sources")); + } + } - /** - * Added one or more source snippets, available from javadoc using - * - * @@tagName. - * - * @param source the class where the snippet is found - * @param tagNames the tag names which should be included - */ - public void addSourceSnippets( Class source, String... tagNames ) - { - for ( String tagName : tagNames ) - { - addSnippet( tagName, sourceSnippet( tagName, source, "sources" ) ); - } - } + /** + * Added one or more source snippets, available from javadoc using + * + * @param source the class where the snippet is found + * @param tagNames the tag names which should be included + * @@tagName. + */ + public void addSourceSnippets(Class source, String... tagNames) { + for (String tagName : tagNames) { + addSnippet(tagName, sourceSnippet(tagName, source, "sources")); + } + } - private static String sourceSnippet( String tagName, Class source, - String classifier ) - { - return "[snippet,java]\n" + "----\n" - + "component=${project.artifactId}\n" + "source=" - + getPath( source ) + "\n" + "classifier=" + classifier + "\n" - + "tag=" + tagName + "\n" + "----\n"; - } + private static String sourceSnippet(String tagName, Class source, + String classifier) { + return "[snippet,java]\n" + "----\n" + + "component=${project.artifactId}\n" + "source=" + + getPath(source) + "\n" + "classifier=" + classifier + "\n" + + "tag=" + tagName + "\n" + "----\n"; + } - public void addGithubTestSourceLink( String key, Class source, - String dir ) - { - githubLink( key, source, dir, "test" ); - } + public void addGithubTestSourceLink(String key, Class source, + String dir) { + githubLink(key, source, dir, "test"); + } - public void addGithubSourceLink( String key, Class source, String dir ) - { - githubLink( key, source, dir, "main" ); - } + public void addGithubSourceLink(String key, Class source, String dir) { + githubLink(key, source, dir, "main"); + } - private void githubLink( String key, Class source, String dir, - String mainOrTest ) - { - String path = "https://github.com/neo4j/neo4j/blob/{neo4j-git-tag}/"; - if ( dir != null ) - { - path += dir + "/"; - } - path += "src/" + mainOrTest + "/java/" + getPath( source ); - path += "[" + source.getSimpleName() + ".java]\n"; - addSnippet( key, path ); - } + private void githubLink(String key, Class source, String dir, + String mainOrTest) { + String path = "https://github.com/neo4j/neo4j/blob/{neo4j-git-tag}/"; + if (dir != null) { + path += dir + "/"; + } + path += "src/" + mainOrTest + "/java/" + getPath(source); + path += "[" + source.getSimpleName() + ".java]\n"; + addSnippet(key, path); + } } diff --git a/src/test/java/org/neo4j/doc/tools/DocumentationData.java b/src/test/java/org/neo4j/doc/tools/DocumentationData.java index e5a80bfdc..2c24d9938 100644 --- a/src/test/java/org/neo4j/doc/tools/DocumentationData.java +++ b/src/test/java/org/neo4j/doc/tools/DocumentationData.java @@ -19,102 +19,86 @@ */ package org.neo4j.doc.tools; -import javax.ws.rs.core.MediaType; import java.util.Map; +import javax.ws.rs.core.MediaType; -class DocumentationData -{ - private String payload; - private MediaType payloadType = MediaType.APPLICATION_JSON_TYPE; - public String title; - public String description; - public String uri; - public String method; - public int status; - public String entity; - public Map requestHeaders; - public Map responseHeaders; - public boolean ignore; - - public void setPayload( final String payload ) - { - this.payload = payload; - } - - public String getPayload() - { - if ( this.payload != null && !this.payload.trim() - .isEmpty() - && MediaType.APPLICATION_JSON_TYPE.equals( payloadType ) ) - { - return JSONPrettifier.parse( this.payload ); - } - else - { - return this.payload; - } - } - - public String getPrettifiedEntity() - { - return JSONPrettifier.parse( entity ); - } - - public void setPayloadType( final MediaType payloadType ) - { - this.payloadType = payloadType; - } - - public void setDescription( final String description ) - { - this.description = description; - } - - public void setTitle( final String title ) - { - this.title = title; - } - - public void setUri( final String uri ) - { - this.uri = uri; - } - - public void setMethod( final String method ) - { - this.method = method; - } - - public void setStatus( final int responseCode ) - { - this.status = responseCode; - - } - - public void setEntity( final String entity ) - { - this.entity = entity; - } - - public void setResponseHeaders( final Map response ) - { - responseHeaders = response; - } - - public void setRequestHeaders( final Map request ) - { - requestHeaders = request; - } - - public void setIgnore() { - this.ignore = true; - } - - @Override - public String toString() - { - return "DocumentationData [payload=" + payload + ", title=" + title + ", description=" + description - + ", uri=" + uri + ", method=" + method + ", status=" + status + ", entity=" + entity - + ", requestHeaders=" + requestHeaders + ", responseHeaders=" + responseHeaders + "]"; - } +class DocumentationData { + + private String payload; + private MediaType payloadType = MediaType.APPLICATION_JSON_TYPE; + public String title; + public String description; + public String uri; + public String method; + public int status; + public String entity; + public Map requestHeaders; + public Map responseHeaders; + public boolean ignore; + + public void setPayload(final String payload) { + this.payload = payload; + } + + public String getPayload() { + if (this.payload != null && !this.payload.trim() + .isEmpty() + && MediaType.APPLICATION_JSON_TYPE.equals(payloadType)) { + return JSONPrettifier.parse(this.payload); + } else { + return this.payload; + } + } + + public String getPrettifiedEntity() { + return JSONPrettifier.parse(entity); + } + + public void setPayloadType(final MediaType payloadType) { + this.payloadType = payloadType; + } + + public void setDescription(final String description) { + this.description = description; + } + + public void setTitle(final String title) { + this.title = title; + } + + public void setUri(final String uri) { + this.uri = uri; + } + + public void setMethod(final String method) { + this.method = method; + } + + public void setStatus(final int responseCode) { + this.status = responseCode; + + } + + public void setEntity(final String entity) { + this.entity = entity; + } + + public void setResponseHeaders(final Map response) { + responseHeaders = response; + } + + public void setRequestHeaders(final Map request) { + requestHeaders = request; + } + + public void setIgnore() { + this.ignore = true; + } + + @Override + public String toString() { + return "DocumentationData [payload=" + payload + ", title=" + title + ", description=" + description + + ", uri=" + uri + ", method=" + method + ", status=" + status + ", entity=" + entity + + ", requestHeaders=" + requestHeaders + ", responseHeaders=" + responseHeaders + "]"; + } } diff --git a/src/test/java/org/neo4j/doc/tools/GraphVizConfig.java b/src/test/java/org/neo4j/doc/tools/GraphVizConfig.java index 609c56967..a8f777939 100644 --- a/src/test/java/org/neo4j/doc/tools/GraphVizConfig.java +++ b/src/test/java/org/neo4j/doc/tools/GraphVizConfig.java @@ -21,81 +21,85 @@ public class GraphVizConfig { - String nodeFontSize = "10"; - String edgeFontSize = nodeFontSize; - String nodeFontColor = "#1c2021"; // darker grey - String edgeFontColor = nodeFontColor; - String edgeColor = "#2e3436"; // dark grey - String boxColor = edgeColor; - String edgeHighlight = "#a40000"; // dark red - String nodeFillColor = "#ffffff"; - String nodeHighlight = "#fcee7d"; // lighter yellow - String nodeHighlight2 = "#fcc574"; // lighter orange - String nodeShape = "box"; - - // Commandline args in the shell script, in order - String fontPath; - // We are not calling the dot command from here at the moment, so we don't need the filename - // String targetImage; - String colorSet; - String graphAttrs; - - String graphSettings = "graph [size=\"7.0,9.0\" fontpath=\"" + fontPath + "\"]"; - String nodeStyle = "filled,rounded"; - String nodeSep = "0.4"; - - String textNode = "shape=plaintext,style=diagonals,height=0.2,margin=0.0,0.0"; - - String arrowHead = "vee"; - String arrowSize = "0.75"; - - String inData; - - String graphFont = "FreeSans"; - String nodeFont = graphFont; - String edgeFont = graphFont; - - public GraphVizConfig(String inData, String fontPath, String colorSet, String graphAttrs) { - this.fontPath = fontPath; - this.colorSet = colorSet; - this.graphAttrs = graphAttrs; - - handleColorSet(colorSet); - - this.inData = handleInData(inData); - } - - public String get() { - String prepend = String.format("digraph g{ %s ", graphSettings) + - String.format("node [shape=\"%s\" penwidth=1.5 fillcolor=\"%s\" color=\"%s\" ", nodeShape, nodeFillColor, boxColor) + - String.format("fontcolor=\"%s\" style=\"%s\" fontsize=%s fontname=\"%s\"] ", nodeFontColor, nodeStyle, nodeFontSize, nodeFont) + - String.format("edge [color=\"%s\" penwidth=2 arrowhead=\"%s\" arrowtail=\"%s\" ", boxColor, arrowHead, arrowHead) + - String.format("arrowSize=%s fontcolor=\"%s\" fontsize=%s fontname=\"%s\"] ", arrowSize, edgeFontColor, edgeFontSize, edgeFont) + - String.format("nodesep=%s fontname=\"%s\"", nodeSep, graphFont) + - graphAttrs; - - return prepend + this.inData + "}"; - } - - private final void handleColorSet(String colorSet) { - if (colorSet.equals("meta")) { - this.nodeFillColor = "#fadcad"; - this.nodeHighlight = "#a8e270"; - this.nodeHighlight2 = "#95bbe3"; - } else if (colorSet.equals("neoviz")) { - this.nodeShape = "Mrecord"; - this.nodeFontSize = "8"; - this.edgeFontSize = this.nodeFontSize; - } - } - - private final String handleInData(String in) { - return in.replace("NODEHIGHLIGHT", nodeHighlight) - .replace("NODE2HIGHLIGHT", nodeHighlight2) - .replace("EDGEHIGHLIGHT", edgeHighlight) - .replace("BOXCOLOR", boxColor) - .replace("TEXTNODE", textNode); - } + String nodeFontSize = "10"; + String edgeFontSize = nodeFontSize; + String nodeFontColor = "#1c2021"; // darker grey + String edgeFontColor = nodeFontColor; + String edgeColor = "#2e3436"; // dark grey + String boxColor = edgeColor; + String edgeHighlight = "#a40000"; // dark red + String nodeFillColor = "#ffffff"; + String nodeHighlight = "#fcee7d"; // lighter yellow + String nodeHighlight2 = "#fcc574"; // lighter orange + String nodeShape = "box"; + + // Commandline args in the shell script, in order + String fontPath; + // We are not calling the dot command from here at the moment, so we don't need the filename + // String targetImage; + String colorSet; + String graphAttrs; + + String graphSettings = "graph [size=\"7.0,9.0\" fontpath=\"" + fontPath + "\"]"; + String nodeStyle = "filled,rounded"; + String nodeSep = "0.4"; + + String textNode = "shape=plaintext,style=diagonals,height=0.2,margin=0.0,0.0"; + + String arrowHead = "vee"; + String arrowSize = "0.75"; + + String inData; + + String graphFont = "FreeSans"; + String nodeFont = graphFont; + String edgeFont = graphFont; + + public GraphVizConfig(String inData, String fontPath, String colorSet, String graphAttrs) { + this.fontPath = fontPath; + this.colorSet = colorSet; + this.graphAttrs = graphAttrs; + + handleColorSet(colorSet); + + this.inData = handleInData(inData); + } + + public String get() { + String prepend = String.format("digraph g{ %s ", graphSettings) + + String.format("node [shape=\"%s\" penwidth=1.5 fillcolor=\"%s\" color=\"%s\" ", nodeShape, + nodeFillColor, boxColor) + + String.format("fontcolor=\"%s\" style=\"%s\" fontsize=%s fontname=\"%s\"] ", nodeFontColor, nodeStyle, + nodeFontSize, nodeFont) + + String.format("edge [color=\"%s\" penwidth=2 arrowhead=\"%s\" arrowtail=\"%s\" ", boxColor, arrowHead, + arrowHead) + + String.format("arrowSize=%s fontcolor=\"%s\" fontsize=%s fontname=\"%s\"] ", arrowSize, edgeFontColor, + edgeFontSize, edgeFont) + + String.format("nodesep=%s fontname=\"%s\"", nodeSep, graphFont) + + graphAttrs; + + return prepend + this.inData + "}"; + } + + private final void handleColorSet(String colorSet) { + if (colorSet.equals("meta")) { + this.nodeFillColor = "#fadcad"; + this.nodeHighlight = "#a8e270"; + this.nodeHighlight2 = "#95bbe3"; + } else if (colorSet.equals("neoviz")) { + this.nodeShape = "Mrecord"; + this.nodeFontSize = "8"; + this.edgeFontSize = this.nodeFontSize; + } + } + + private final String handleInData(String in) { + return in.replace("NODEHIGHLIGHT", nodeHighlight) + .replace("NODE2HIGHLIGHT", nodeHighlight2) + .replace("EDGEHIGHLIGHT", edgeHighlight) + .replace("BOXCOLOR", boxColor) + .replace("TEXTNODE", textNode); + } } diff --git a/src/test/java/org/neo4j/doc/tools/JSONPrettifier.java b/src/test/java/org/neo4j/doc/tools/JSONPrettifier.java index 9e95b3442..c650cfec4 100644 --- a/src/test/java/org/neo4j/doc/tools/JSONPrettifier.java +++ b/src/test/java/org/neo4j/doc/tools/JSONPrettifier.java @@ -30,47 +30,48 @@ * Naive implementation of a JSON prettifier. */ public class JSONPrettifier { - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - private static final JsonParser JSON_PARSER = new JsonParser(); - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final ObjectWriter WRITER = MAPPER.writerWithDefaultPrettyPrinter(); - public static String parse(final String json) { - if (json == null) { - return ""; - } + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final JsonParser JSON_PARSER = new JsonParser(); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ObjectWriter WRITER = MAPPER.writerWithDefaultPrettyPrinter(); - String result = json; + public static String parse(final String json) { + if (json == null) { + return ""; + } - try { - if (json.contains("\"exception\"")) { - // the gson renderer is much better for stacktraces - result = gsonPrettyPrint(json); - } else { - result = jacksonPrettyPrint(json); - } - } catch (Exception e) { - /* - * Enable the output to see where exceptions happen. - * We need to be able to tell the rest docs tools to expect - * a json parsing error from here, then we can simply throw an exception instead. - * (we have tests sending in broken json to test the response) - */ - // System.out.println( "***************************************" ); - // System.out.println( json ); - // System.out.println( "***************************************" ); - } - return result; - } + String result = json; - private static String gsonPrettyPrint(final String json) { - JsonElement element = JSON_PARSER.parse(json); - return GSON.toJson(element); - } + try { + if (json.contains("\"exception\"")) { + // the gson renderer is much better for stacktraces + result = gsonPrettyPrint(json); + } else { + result = jacksonPrettyPrint(json); + } + } catch (Exception e) { + /* + * Enable the output to see where exceptions happen. + * We need to be able to tell the rest docs tools to expect + * a json parsing error from here, then we can simply throw an exception instead. + * (we have tests sending in broken json to test the response) + */ + // System.out.println( "***************************************" ); + // System.out.println( json ); + // System.out.println( "***************************************" ); + } + return result; + } - private static String jacksonPrettyPrint(final String json) - throws Exception { - Object myObject = MAPPER.readValue(json, Object.class); - return WRITER.writeValueAsString(myObject); - } + private static String gsonPrettyPrint(final String json) { + JsonElement element = JSON_PARSER.parse(json); + return GSON.toJson(element); + } + + private static String jacksonPrettyPrint(final String json) + throws Exception { + Object myObject = MAPPER.readValue(json, Object.class); + return WRITER.writeValueAsString(myObject); + } } diff --git a/src/test/java/org/neo4j/doc/tools/JavaTestDocsGenerator.java b/src/test/java/org/neo4j/doc/tools/JavaTestDocsGenerator.java index f68255d8f..d30982e78 100644 --- a/src/test/java/org/neo4j/doc/tools/JavaTestDocsGenerator.java +++ b/src/test/java/org/neo4j/doc/tools/JavaTestDocsGenerator.java @@ -19,11 +19,10 @@ */ package org.neo4j.doc.tools; -import org.neo4j.test.TestData.Producer; - import java.io.File; import java.io.IOException; import java.io.Writer; +import org.neo4j.test.TestData.Producer; /** * This class is supporting the generation of ASCIIDOC documentation @@ -31,39 +30,41 @@ * and will replace their @@snippetName placeholders in the documentation description. */ public class JavaTestDocsGenerator extends AsciiDocGenerator { - public static final Producer PRODUCER = (graph, title, documentation) -> (JavaTestDocsGenerator) new JavaTestDocsGenerator(title).description(documentation); - public JavaTestDocsGenerator(String title) { - super(title, "docs"); - } + public static final Producer PRODUCER = (graph, title, documentation) -> (JavaTestDocsGenerator) new JavaTestDocsGenerator( + title).description(documentation); + + public JavaTestDocsGenerator(String title) { + super(title, "docs"); + } - public void document(String directory, String sectionName) { - this.setSection(sectionName); - String name = title.replace(" ", "-").toLowerCase(); - File dir = new File(new File(directory), section); - String filename = name + ".asciidoc"; - Writer fw = getFW(dir, filename); - description = replaceSnippets(description, dir, name); - try { - line(fw, "[[" + sectionName + "-" + name.replaceAll("[()]", "") + "]]"); - String firstChar = title.substring(0, 1).toUpperCase(); - line(fw, firstChar + title.substring(1)); - for (int i = 0; i < title.length(); i++) { - fw.append("="); - } - fw.append("\n"); - line(fw, ""); - line(fw, description); - line(fw, ""); - fw.flush(); - fw.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } + public void document(String directory, String sectionName) { + this.setSection(sectionName); + String name = title.replace(" ", "-").toLowerCase(); + File dir = new File(new File(directory), section); + String filename = name + ".asciidoc"; + Writer fw = getFW(dir, filename); + description = replaceSnippets(description, dir, name); + try { + line(fw, "[[" + sectionName + "-" + name.replaceAll("[()]", "") + "]]"); + String firstChar = title.substring(0, 1).toUpperCase(); + line(fw, firstChar + title.substring(1)); + for (int i = 0; i < title.length(); i++) { + fw.append("="); + } + fw.append("\n"); + line(fw, ""); + line(fw, description); + line(fw, ""); + fw.flush(); + fw.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } - public void addImageSnippet(String tagName, String imageName, String title) { - this.addSnippet(tagName, "\nimage:" + imageName + "[" + title + "]\n"); - } + public void addImageSnippet(String tagName, String imageName, String title) { + this.addSnippet(tagName, "\nimage:" + imageName + "[" + title + "]\n"); + } } diff --git a/src/test/java/org/neo4j/doc/tools/SpatialGraphVizHelper.java b/src/test/java/org/neo4j/doc/tools/SpatialGraphVizHelper.java index 96f28f954..170384d20 100644 --- a/src/test/java/org/neo4j/doc/tools/SpatialGraphVizHelper.java +++ b/src/test/java/org/neo4j/doc/tools/SpatialGraphVizHelper.java @@ -19,6 +19,10 @@ */ package org.neo4j.doc.tools; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; import org.neo4j.visualization.graphviz.AsciiDocStyle; @@ -26,57 +30,53 @@ import org.neo4j.visualization.graphviz.GraphvizWriter; import org.neo4j.walk.Walker; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; - public class SpatialGraphVizHelper extends org.neo4j.visualization.asciidoc.AsciidocHelper { - private static final String ILLEGAL_STRINGS = "[:\\(\\)\t;&/\\\\]"; + private static final String ILLEGAL_STRINGS = "[:\\(\\)\t;&/\\\\]"; - public static String createGraphVizWithNodeId( - String title, GraphDatabaseService graph, String identifier - ) { - return createGraphViz( - title, graph, identifier, AsciiDocStyle.withAutomaticRelationshipTypeColors(), "" - ); - } + public static String createGraphVizWithNodeId( + String title, GraphDatabaseService graph, String identifier + ) { + return createGraphViz( + title, graph, identifier, AsciiDocStyle.withAutomaticRelationshipTypeColors(), "" + ); + } - public static String createGraphViz(String title, GraphDatabaseService graph, String identifier, GraphStyle graphStyle, String graphvizOptions) { - try (Transaction tx = graph.beginTx()) { - GraphvizWriter writer = new GraphvizWriter( graphStyle ); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - writer.emit( out, Walker.fullGraph( graph ) ); - } catch (IOException e) { - e.printStackTrace(); - } + public static String createGraphViz(String title, GraphDatabaseService graph, String identifier, + GraphStyle graphStyle, String graphvizOptions) { + try (Transaction tx = graph.beginTx()) { + GraphvizWriter writer = new GraphvizWriter(graphStyle); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + writer.emit(out, Walker.fullGraph(graph)); + } catch (IOException e) { + e.printStackTrace(); + } - String safeTitle = title.replaceAll( ILLEGAL_STRINGS, "" ); + String safeTitle = title.replaceAll(ILLEGAL_STRINGS, ""); - tx.commit(); + tx.commit(); - String fontsDir = "target/tools/bin/fonts"; - String colorSet = "neoviz"; - String graphAttrs = ""; + String fontsDir = "target/tools/bin/fonts"; + String colorSet = "neoviz"; + String graphAttrs = ""; - try { - String result = "." + title + "\n[graphviz, " - + (safeTitle + "-" + identifier).replace( " ", "-" ) - + ", svg]\n" - + "----\n" + - new GraphVizConfig( - out.toString( StandardCharsets.UTF_8.name()), - fontsDir, - colorSet, graphAttrs - ).get() + "\n" + - "----\n"; - System.out.println(result); - return result; - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - } + try { + String result = "." + title + "\n[graphviz, " + + (safeTitle + "-" + identifier).replace(" ", "-") + + ", svg]\n" + + "----\n" + + new GraphVizConfig( + out.toString(StandardCharsets.UTF_8.name()), + fontsDir, + colorSet, graphAttrs + ).get() + "\n" + + "----\n"; + System.out.println(result); + return result; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/AbstractJavaDocTestBase.java b/src/test/java/org/neo4j/gis/spatial/AbstractJavaDocTestBase.java index 620afe55b..8c942ef65 100644 --- a/src/test/java/org/neo4j/gis/spatial/AbstractJavaDocTestBase.java +++ b/src/test/java/org/neo4j/gis/spatial/AbstractJavaDocTestBase.java @@ -19,6 +19,7 @@ */ package org.neo4j.gis.spatial; +import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -32,45 +33,44 @@ import org.neo4j.test.GraphHolder; import org.neo4j.test.TestData; -import java.util.Map; - /** * This class was copied from the class of the same name in neo4j-examples, in order to reduce the dependency chain */ public abstract class AbstractJavaDocTestBase implements GraphHolder { - @RegisterExtension - public TestData> data = TestData.producedThrough(GraphDescription.createGraphFor(this)); - @RegisterExtension - public TestData gen = TestData.producedThrough(JavaTestDocsGenerator.PRODUCER); - protected static DatabaseManagementService databases; - protected static GraphDatabaseService db; - @AfterAll - public static void shutdownDb() { - try { - if (databases != null) { - databases.shutdown(); - } - } finally { - databases = null; - db = null; - } + @RegisterExtension + public TestData> data = TestData.producedThrough(GraphDescription.createGraphFor(this)); + @RegisterExtension + public TestData gen = TestData.producedThrough(JavaTestDocsGenerator.PRODUCER); + protected static DatabaseManagementService databases; + protected static GraphDatabaseService db; + + @AfterAll + public static void shutdownDb() { + try { + if (databases != null) { + databases.shutdown(); + } + } finally { + databases = null; + db = null; + } - } + } - public GraphDatabaseService graphdb() { - return db; - } + public GraphDatabaseService graphdb() { + return db; + } - @BeforeEach - public void setUp() { - GraphDatabaseService graphdb = this.graphdb(); - GraphDatabaseServiceCleaner.cleanDatabaseContent(graphdb); - this.gen.get().setGraph(graphdb); - } + @BeforeEach + public void setUp() { + GraphDatabaseService graphdb = this.graphdb(); + GraphDatabaseServiceCleaner.cleanDatabaseContent(graphdb); + this.gen.get().setGraph(graphdb); + } - @AfterEach - public void doc() { - this.gen.get().document("target/docs/dev", "examples"); - } + @AfterEach + public void doc() { + this.gen.get().document("target/docs/dev", "examples"); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/FakeIndex.java b/src/test/java/org/neo4j/gis/spatial/FakeIndex.java index 0654ef719..1eb4208d4 100644 --- a/src/test/java/org/neo4j/gis/spatial/FakeIndex.java +++ b/src/test/java/org/neo4j/gis/spatial/FakeIndex.java @@ -19,8 +19,12 @@ */ package org.neo4j.gis.spatial; -import java.util.*; - +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.index.LayerIndexReader; import org.neo4j.gis.spatial.rtree.Envelope; @@ -28,7 +32,6 @@ import org.neo4j.gis.spatial.rtree.TreeMonitor; import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.gis.spatial.rtree.filter.SearchResults; -import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; @@ -37,152 +40,152 @@ */ public class FakeIndex implements LayerIndexReader, Constants { - public FakeIndex(Layer layer, IndexManager indexManager) { - init(null, indexManager, layer); - } - - @Override - public void init(Transaction ignored, IndexManager indexManager, Layer layer) { - this.layer = layer; - } - - @Override - public Layer getLayer() { - return layer; - } - - @Override - public int count(Transaction tx) { - int count = 0; - - // @TODO: Consider adding a count method to Layer or SpatialDataset to allow for - // optimization of this if this kind of code gets used elsewhere - for (@SuppressWarnings("unused") Node node : layer.getDataset().getAllGeometryNodes(tx)) { - count++; - } - - return count; - } - - @Override - public boolean isEmpty(Transaction tx) { - return count(tx) == 0; - } - - @Override - public Envelope getBoundingBox(Transaction tx) { - Envelope bbox = null; - - GeometryEncoder geomEncoder = layer.getGeometryEncoder(); - for (Node node : layer.getDataset().getAllGeometryNodes(tx)) { - if (bbox == null) { - bbox = geomEncoder.decodeEnvelope(node); - } else { - bbox.expandToInclude(geomEncoder.decodeEnvelope(node)); - } - } - - return bbox; - } - - public SpatialDatabaseRecord get(Transaction tx, String geomNodeId) { - return new SpatialDatabaseRecord(layer, tx.getNodeByElementId(geomNodeId)); - } - - public List get(Transaction tx, Set geomNodeIds) { - List results = new ArrayList<>(); - - for (String geomNodeId : geomNodeIds) { - results.add(get(tx, geomNodeId)); - } - - return results; - } - - @Override - public Iterable getAllIndexedNodes(Transaction tx) { - return layer.getIndex().getAllIndexedNodes(tx); - } - - @Override - public EnvelopeDecoder getEnvelopeDecoder() { - return layer.getGeometryEncoder(); - } - - - @Override - public boolean isNodeIndexed(Transaction tx, String nodeId) { - // TODO - return true; - } - - - // Attributes - - private Layer layer; - - private class NodeFilter implements Iterable, Iterator { - private final Transaction tx; - private final SearchFilter filter; - private Node nextNode; - private final Iterator nodes; - - NodeFilter(Transaction tx, SearchFilter filter, Iterable nodes) { - this.tx = tx; - this.filter = filter; - this.nodes = nodes.iterator(); - nextNode = getNextNode(); - } - - @Override - public Iterator iterator() { - return this; - } - - @Override - public boolean hasNext() { - return nextNode != null; - } - - @Override - public Node next() { - Node currentNode = nextNode; - nextNode = getNextNode(); - return currentNode; - } - - private Node getNextNode() { - Node nn = null; - while (nodes.hasNext()) { - Node node = nodes.next(); - if (filter.geometryMatches(tx, node)) { - nn = node; - break; - } - } - return nn; - } - - @Override - public void remove() { - } - } - - @Override - public SearchResults searchIndex(Transaction tx, SearchFilter filter) { - return new SearchResults(new NodeFilter(tx, filter, layer.getDataset().getAllGeometryNodes(tx))); - } - - @Override - public void addMonitor(TreeMonitor monitor) { - } - - @Override - public void configure(Map config) { - } - - @Override - public SearchRecords search(Transaction tx, SearchFilter filter) { - return new SearchRecords(layer, searchIndex(tx, filter)); - } + public FakeIndex(Layer layer, IndexManager indexManager) { + init(null, indexManager, layer); + } + + @Override + public void init(Transaction ignored, IndexManager indexManager, Layer layer) { + this.layer = layer; + } + + @Override + public Layer getLayer() { + return layer; + } + + @Override + public int count(Transaction tx) { + int count = 0; + + // @TODO: Consider adding a count method to Layer or SpatialDataset to allow for + // optimization of this if this kind of code gets used elsewhere + for (@SuppressWarnings("unused") Node node : layer.getDataset().getAllGeometryNodes(tx)) { + count++; + } + + return count; + } + + @Override + public boolean isEmpty(Transaction tx) { + return count(tx) == 0; + } + + @Override + public Envelope getBoundingBox(Transaction tx) { + Envelope bbox = null; + + GeometryEncoder geomEncoder = layer.getGeometryEncoder(); + for (Node node : layer.getDataset().getAllGeometryNodes(tx)) { + if (bbox == null) { + bbox = geomEncoder.decodeEnvelope(node); + } else { + bbox.expandToInclude(geomEncoder.decodeEnvelope(node)); + } + } + + return bbox; + } + + public SpatialDatabaseRecord get(Transaction tx, String geomNodeId) { + return new SpatialDatabaseRecord(layer, tx.getNodeByElementId(geomNodeId)); + } + + public List get(Transaction tx, Set geomNodeIds) { + List results = new ArrayList<>(); + + for (String geomNodeId : geomNodeIds) { + results.add(get(tx, geomNodeId)); + } + + return results; + } + + @Override + public Iterable getAllIndexedNodes(Transaction tx) { + return layer.getIndex().getAllIndexedNodes(tx); + } + + @Override + public EnvelopeDecoder getEnvelopeDecoder() { + return layer.getGeometryEncoder(); + } + + + @Override + public boolean isNodeIndexed(Transaction tx, String nodeId) { + // TODO + return true; + } + + // Attributes + + private Layer layer; + + private class NodeFilter implements Iterable, Iterator { + + private final Transaction tx; + private final SearchFilter filter; + private Node nextNode; + private final Iterator nodes; + + NodeFilter(Transaction tx, SearchFilter filter, Iterable nodes) { + this.tx = tx; + this.filter = filter; + this.nodes = nodes.iterator(); + nextNode = getNextNode(); + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public boolean hasNext() { + return nextNode != null; + } + + @Override + public Node next() { + Node currentNode = nextNode; + nextNode = getNextNode(); + return currentNode; + } + + private Node getNextNode() { + Node nn = null; + while (nodes.hasNext()) { + Node node = nodes.next(); + if (filter.geometryMatches(tx, node)) { + nn = node; + break; + } + } + return nn; + } + + @Override + public void remove() { + } + } + + @Override + public SearchResults searchIndex(Transaction tx, SearchFilter filter) { + return new SearchResults(new NodeFilter(tx, filter, layer.getDataset().getAllGeometryNodes(tx))); + } + + @Override + public void addMonitor(TreeMonitor monitor) { + } + + @Override + public void configure(Map config) { + } + + @Override + public SearchRecords search(Transaction tx, SearchFilter filter) { + return new SearchRecords(layer, searchIndex(tx, filter)); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/LayerSignatureTest.java b/src/test/java/org/neo4j/gis/spatial/LayerSignatureTest.java index 7c7d23297..1520d6395 100644 --- a/src/test/java/org/neo4j/gis/spatial/LayerSignatureTest.java +++ b/src/test/java/org/neo4j/gis/spatial/LayerSignatureTest.java @@ -19,6 +19,10 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Consumer; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.gis.spatial.index.IndexManager; @@ -26,83 +30,86 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertEquals; - public class LayerSignatureTest extends Neo4jTestCase implements Constants { - private SpatialDatabaseService spatial; - @BeforeEach - public void setup() throws Exception { - super.setUp(); - spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - } + private SpatialDatabaseService spatial; + + @BeforeEach + public void setup() throws Exception { + super.setUp(); + spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + } - @Test - public void testSimplePointLayer() { - testLayerSignature("EditableLayer(name='test', encoder=SimplePointEncoder(x='lng', y='lat', bbox='bbox'))", - tx -> spatial.createSimplePointLayer(tx, "test", "lng", "lat")); - } + @Test + public void testSimplePointLayer() { + testLayerSignature("EditableLayer(name='test', encoder=SimplePointEncoder(x='lng', y='lat', bbox='bbox'))", + tx -> spatial.createSimplePointLayer(tx, "test", "lng", "lat")); + } - @Test - public void testNativePointLayer() { - testLayerSignature("EditableLayer(name='test', encoder=NativePointEncoder(geometry='position', bbox='mbr', crs=4326))", - tx -> spatial.createNativePointLayer(tx, "test", "position", "mbr")); - } + @Test + public void testNativePointLayer() { + testLayerSignature( + "EditableLayer(name='test', encoder=NativePointEncoder(geometry='position', bbox='mbr', crs=4326))", + tx -> spatial.createNativePointLayer(tx, "test", "position", "mbr")); + } - @Test - public void testDefaultSimplePointLayer() { - testLayerSignature("EditableLayer(name='test', encoder=SimplePointEncoder(x='longitude', y='latitude', bbox='bbox'))", - tx -> spatial.createSimplePointLayer(tx, "test")); - } + @Test + public void testDefaultSimplePointLayer() { + testLayerSignature( + "EditableLayer(name='test', encoder=SimplePointEncoder(x='longitude', y='latitude', bbox='bbox'))", + tx -> spatial.createSimplePointLayer(tx, "test")); + } - @Test - public void testSimpleWKBLayer() { - testLayerSignature("EditableLayer(name='test', encoder=WKBGeometryEncoder(geom='geometry', bbox='bbox'))", - tx -> spatial.createWKBLayer(tx, "test")); - } + @Test + public void testSimpleWKBLayer() { + testLayerSignature("EditableLayer(name='test', encoder=WKBGeometryEncoder(geom='geometry', bbox='bbox'))", + tx -> spatial.createWKBLayer(tx, "test")); + } - @Test - public void testWKBLayer() { - testLayerSignature("EditableLayer(name='test', encoder=WKBGeometryEncoder(geom='wkb', bbox='bbox'))", - tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkb", "wkb")); - } + @Test + public void testWKBLayer() { + testLayerSignature("EditableLayer(name='test', encoder=WKBGeometryEncoder(geom='wkb', bbox='bbox'))", + tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkb", "wkb")); + } - @Test - public void testWKTLayer() { - testLayerSignature("EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", - tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkt", "wkt")); - } + @Test + public void testWKTLayer() { + testLayerSignature("EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", + tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkt", "wkt")); + } - private Layer testLayerSignature(String signature, Function layerMaker) { - Layer layer; - try (Transaction tx = graphDb().beginTx()) { - layer = layerMaker.apply(tx); - tx.commit(); - } - assertEquals(signature, layer.getSignature()); - return layer; - } + private Layer testLayerSignature(String signature, Function layerMaker) { + Layer layer; + try (Transaction tx = graphDb().beginTx()) { + layer = layerMaker.apply(tx); + tx.commit(); + } + assertEquals(signature, layer.getSignature()); + return layer; + } - private void inTx(Consumer txFunction) { - try (Transaction tx = graphDb().beginTx()) { - txFunction.accept(tx); - tx.commit(); - } - } + private void inTx(Consumer txFunction) { + try (Transaction tx = graphDb().beginTx()) { + txFunction.accept(tx); + tx.commit(); + } + } - @Test - public void testDynamicLayer() { - Layer layer = testLayerSignature("EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", - tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkt", "wkt")); - inTx(tx -> { - DynamicLayer dynamic = spatial.asDynamicLayer(tx, layer); - assertEquals("EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", dynamic.getSignature()); - DynamicLayerConfig points = dynamic.addCQLDynamicLayerOnAttribute(tx, "is_a", "point", GTYPE_POINT); - assertEquals("DynamicLayer(name='CQL:is_a-point', config={layer='CQL:is_a-point', query=\"geometryType(the_geom) = 'Point' AND is_a = 'point'\"})", points.getSignature()); - }); - } + @Test + public void testDynamicLayer() { + Layer layer = testLayerSignature( + "EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", + tx -> spatial.getOrCreateEditableLayer(tx, "test", "wkt", "wkt")); + inTx(tx -> { + DynamicLayer dynamic = spatial.asDynamicLayer(tx, layer); + assertEquals("EditableLayer(name='test', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", + dynamic.getSignature()); + DynamicLayerConfig points = dynamic.addCQLDynamicLayerOnAttribute(tx, "is_a", "point", GTYPE_POINT); + assertEquals( + "DynamicLayer(name='CQL:is_a-point', config={layer='CQL:is_a-point', query=\"geometryType(the_geom) = 'Point' AND is_a = 'point'\"})", + points.getSignature()); + }); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/LayersTest.java b/src/test/java/org/neo4j/gis/spatial/LayersTest.java index 04232cd65..e4acec3f0 100644 --- a/src/test/java/org/neo4j/gis/spatial/LayersTest.java +++ b/src/test/java/org/neo4j/gis/spatial/LayersTest.java @@ -19,6 +19,18 @@ */ package org.neo4j.gis.spatial; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,320 +53,351 @@ import org.neo4j.gis.spatial.pipes.GeoPipeline; import org.neo4j.gis.spatial.procedures.SpatialProcedures; import org.neo4j.gis.spatial.rtree.ProgressLoggingListener; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.ResourceIterator; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.api.procedure.GlobalProcedures; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.function.Consumer; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - public class LayersTest { - private DatabaseManagementService databases; - private GraphDatabaseService graphDb; - - @BeforeEach - public void setup() throws KernelException { - databases = new TestDatabaseManagementServiceBuilder(new File("target/layers").toPath()).impermanent().build(); - graphDb = databases.database(DEFAULT_DATABASE_NAME); - ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(GlobalProcedures.class).registerProcedure(SpatialProcedures.class); - } - - @AfterEach - public void teardown() { - databases.shutdown(); - } - - @Test - public void testBasicLayerOperations() { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - Layer layer = spatial.getLayer(tx, layerName); - assertNull(layer); - }); - inTx(tx -> { - Layer layer = spatial.createWKBLayer(tx, layerName); - assertNotNull(layer); - assertThat("Should be a default layer", layer instanceof DefaultLayer); - }); - inTx(tx -> spatial.deleteLayer(tx, layerName, new ProgressLoggingListener("deleting layer '" + layerName + "'", System.out))); - inTx(tx -> assertNull(spatial.getLayer(tx, layerName))); - } - - @Test - public void testSimplePointLayerWithRTree() { - testPointLayer(LayerRTreeIndex.class, SimplePointEncoder.class); - } - - @Test - public void testSimplePointLayerWithGeohash() { - testPointLayer(LayerGeohashPointIndex.class, SimplePointEncoder.class); - } - - @Test - public void testNativePointLayerWithRTree() { - testPointLayer(LayerRTreeIndex.class, NativePointEncoder.class); - } - - @Test - public void testNativePointLayerWithGeohash() { - testPointLayer(LayerGeohashPointIndex.class, NativePointEncoder.class); - } - - private void testPointLayer(Class indexClass, Class encoderClass) { - String layerName = "points"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer layer = (EditableLayer) spatial.createLayer(tx, layerName, encoderClass, EditableLayerImpl.class, indexClass, null); - assertNotNull(layer); - }); - inTx(tx -> { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - SpatialDatabaseRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); - assertNotNull(record); - }); - // finds geometries that contain the given geometry - try (Transaction tx = graphDb.beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - List results = GeoPipeline - .startContainSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) - .toSpatialDatabaseRecordList(); - - // should not be contained - assertEquals(0, results.size()); - - results = GeoPipeline - .startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) - .toSpatialDatabaseRecordList(); - - assertEquals(1, results.size()); - tx.commit(); - } - inTx(tx -> spatial.deleteLayer(tx, layerName, new ProgressLoggingListener("deleting layer '" + layerName + "'", System.out))); - inTx(tx -> assertNull(spatial.getLayer(tx, layerName))); - spatial.indexManager.waitForDeletions(); - } - - @Test - public void testDeleteSimplePointGeometry() { - testDeleteGeometry(SimplePointEncoder.class); - } - - @Test - public void testDeleteNativePointGeometry() { - testDeleteGeometry(NativePointEncoder.class); - } - - private void testDeleteGeometry(Class encoderClass) { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer layer = (EditableLayer) spatial.createLayer(tx, layerName, encoderClass, EditableLayerImpl.class, null, null); - assertNotNull(layer); - }); - inTx(tx -> { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - SpatialDatabaseRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); - assertNotNull(record); - // try to remove the geometry - layer.delete(tx, record.getNodeId()); - }); - } - - @Test - public void testEditableLayer() { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer layer = spatial.getOrCreateEditableLayer(tx, layerName); - assertNotNull(layer); - }); - inTx(tx -> { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - SpatialDatabaseRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); - assertNotNull(record); - }); - - try (Transaction tx = graphDb.beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - - // finds geometries that contain the given geometry - List results = GeoPipeline - .startContainSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) - .toSpatialDatabaseRecordList(); - - // should not be contained - assertEquals(0, results.size()); - - results = GeoPipeline - .startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) - .toSpatialDatabaseRecordList(); - - assertEquals(1, results.size()); - tx.commit(); - } - } - - @Test - public void testSnapToLine() { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer layer = spatial.getOrCreateEditableLayer(tx, "roads"); - Coordinate crossing_bygg_forstadsgatan = new Coordinate(13.0171471, 55.6074148); - Coordinate[] waypoints_forstadsgatan = {new Coordinate(13.0201511, 55.6066846), crossing_bygg_forstadsgatan}; - LineString ostra_forstadsgatan_malmo = layer.getGeometryFactory().createLineString(waypoints_forstadsgatan); - Coordinate[] waypoints_byggmastaregatan = {crossing_bygg_forstadsgatan, new Coordinate(13.0182092, 55.6088238)}; - LineString byggmastaregatan_malmo = layer.getGeometryFactory().createLineString(waypoints_byggmastaregatan); - LineString[] test_way_segments = {byggmastaregatan_malmo, ostra_forstadsgatan_malmo}; - /* MultiLineString test_way = */ - layer.getGeometryFactory().createMultiLineString(test_way_segments); - }); - inTx(tx -> { - // Coordinate slussgatan14 = new Coordinate( 13.0181127, 55.608236 ); - //TODO now determine the nearest point on test_way to slussis - }); - } - - @Test - public void testEditableLayers() { - testSpecificEditableLayer("test dynamic layer with property encoder", SimplePropertyEncoder.class, DynamicLayer.class); - testSpecificEditableLayer("test dynamic layer with graph encoder", SimpleGraphEncoder.class, DynamicLayer.class); - testSpecificEditableLayer("test OSM layer with OSM encoder", OSMGeometryEncoder.class, OSMLayer.class); - testSpecificEditableLayer("test editable layer with property encoder", SimplePropertyEncoder.class, EditableLayerImpl.class); - testSpecificEditableLayer("test editable layer with graph encoder", SimpleGraphEncoder.class, EditableLayerImpl.class); - testSpecificEditableLayer("test editable layer with OSM encoder", OSMGeometryEncoder.class, EditableLayerImpl.class); - } - - private String testSpecificEditableLayer(String layerName, Class geometryEncoderClass, Class layerClass) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - Layer layer = spatial.createLayer(tx, layerName, geometryEncoderClass, layerClass); - assertNotNull(layer); - assertTrue(layer instanceof EditableLayer, "Should be an editable layer"); - }); - inTx(tx -> { - Layer layer = spatial.getLayer(tx, layerName); - assertNotNull(layer); - assertTrue(layer instanceof EditableLayer, "Should be an editable layer"); - EditableLayer editableLayer = (EditableLayer) layer; - - CoordinateList coordinates = new CoordinateList(); - coordinates.add(new Coordinate(13.1, 56.2), false); - coordinates.add(new Coordinate(13.2, 56.0), false); - coordinates.add(new Coordinate(13.3, 56.2), false); - coordinates.add(new Coordinate(13.2, 56.0), false); - coordinates.add(new Coordinate(13.1, 56.2), false); - coordinates.add(new Coordinate(13.0, 56.0), false); - editableLayer.add(tx, layer.getGeometryFactory().createLineString(coordinates.toCoordinateArray())); - - coordinates = new CoordinateList(); - coordinates.add(new Coordinate(14.1, 56.0), false); - coordinates.add(new Coordinate(14.3, 56.1), false); - coordinates.add(new Coordinate(14.2, 56.1), false); - coordinates.add(new Coordinate(14.0, 56.0), false); - editableLayer.add(tx, layer.getGeometryFactory().createLineString(coordinates.toCoordinateArray())); - }); - - // TODO this test is not complete - - try (Transaction tx = graphDb.beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - printResults(layer, GeoPipeline - .startIntersectSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(13.2, 14.1, 56.1, 56.2))) - .toSpatialDatabaseRecordList()); - - printResults(layer, GeoPipeline - .startContainSearch(tx, layer, layer.getGeometryFactory().toGeometry(new Envelope(12.0, 15.0, 55.0, 57.0))) - .toSpatialDatabaseRecordList()); - tx.commit(); - } - return layerName; - } - - private void printResults(Layer layer, List results) { - System.out.println("\tTesting layer '" + layer.getName() + "' (class " + layer.getClass() + "), found results: " + results.size()); - for (SpatialDatabaseRecord r : results) { - System.out.println("\t\tGeometry: " + r); - } - } - - @Test - public void testShapefileExport() throws Exception { - ShapefileExporter exporter = new ShapefileExporter(graphDb); - exporter.setExportDir("target/export"); - ArrayList layers = new ArrayList<>(); - - layers.add(testSpecificEditableLayer("test dynamic layer with property encoder", SimplePropertyEncoder.class, DynamicLayer.class)); - layers.add(testSpecificEditableLayer("test dynamic layer with graph encoder", SimpleGraphEncoder.class, DynamicLayer.class)); - layers.add(testSpecificEditableLayer("test dynamic layer with OSM encoder", OSMGeometryEncoder.class, OSMLayer.class)); - - for (String layerName : layers) { - exporter.exportLayer(layerName); - } - } - - @Test - public void testIndexAccessAfterBulkInsertion() { - // Use these two lines if you want to examine the output. + + private DatabaseManagementService databases; + private GraphDatabaseService graphDb; + + @BeforeEach + public void setup() throws KernelException { + databases = new TestDatabaseManagementServiceBuilder(new File("target/layers").toPath()).impermanent().build(); + graphDb = databases.database(DEFAULT_DATABASE_NAME); + ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(GlobalProcedures.class) + .registerProcedure(SpatialProcedures.class); + } + + @AfterEach + public void teardown() { + databases.shutdown(); + } + + @Test + public void testBasicLayerOperations() { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + Layer layer = spatial.getLayer(tx, layerName); + assertNull(layer); + }); + inTx(tx -> { + Layer layer = spatial.createWKBLayer(tx, layerName); + assertNotNull(layer); + assertThat("Should be a default layer", layer instanceof DefaultLayer); + }); + inTx(tx -> spatial.deleteLayer(tx, layerName, + new ProgressLoggingListener("deleting layer '" + layerName + "'", System.out))); + inTx(tx -> assertNull(spatial.getLayer(tx, layerName))); + } + + @Test + public void testSimplePointLayerWithRTree() { + testPointLayer(LayerRTreeIndex.class, SimplePointEncoder.class); + } + + @Test + public void testSimplePointLayerWithGeohash() { + testPointLayer(LayerGeohashPointIndex.class, SimplePointEncoder.class); + } + + @Test + public void testNativePointLayerWithRTree() { + testPointLayer(LayerRTreeIndex.class, NativePointEncoder.class); + } + + @Test + public void testNativePointLayerWithGeohash() { + testPointLayer(LayerGeohashPointIndex.class, NativePointEncoder.class); + } + + private void testPointLayer(Class indexClass, + Class encoderClass) { + String layerName = "points"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer layer = (EditableLayer) spatial.createLayer(tx, layerName, encoderClass, + EditableLayerImpl.class, indexClass, null); + assertNotNull(layer); + }); + inTx(tx -> { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + SpatialDatabaseRecord record = layer.add(tx, + layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); + assertNotNull(record); + }); + // finds geometries that contain the given geometry + try (Transaction tx = graphDb.beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + List results = GeoPipeline + .startContainSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) + .toSpatialDatabaseRecordList(); + + // should not be contained + assertEquals(0, results.size()); + + results = GeoPipeline + .startWithinSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) + .toSpatialDatabaseRecordList(); + + assertEquals(1, results.size()); + tx.commit(); + } + inTx(tx -> spatial.deleteLayer(tx, layerName, + new ProgressLoggingListener("deleting layer '" + layerName + "'", System.out))); + inTx(tx -> assertNull(spatial.getLayer(tx, layerName))); + spatial.indexManager.waitForDeletions(); + } + + @Test + public void testDeleteSimplePointGeometry() { + testDeleteGeometry(SimplePointEncoder.class); + } + + @Test + public void testDeleteNativePointGeometry() { + testDeleteGeometry(NativePointEncoder.class); + } + + private void testDeleteGeometry(Class encoderClass) { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer layer = (EditableLayer) spatial.createLayer(tx, layerName, encoderClass, + EditableLayerImpl.class, null, null); + assertNotNull(layer); + }); + inTx(tx -> { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + SpatialDatabaseRecord record = layer.add(tx, + layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); + assertNotNull(record); + // try to remove the geometry + layer.delete(tx, record.getNodeId()); + }); + } + + @Test + public void testEditableLayer() { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer layer = spatial.getOrCreateEditableLayer(tx, layerName); + assertNotNull(layer); + }); + inTx(tx -> { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + SpatialDatabaseRecord record = layer.add(tx, + layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); + assertNotNull(record); + }); + + try (Transaction tx = graphDb.beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + + // finds geometries that contain the given geometry + List results = GeoPipeline + .startContainSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) + .toSpatialDatabaseRecordList(); + + // should not be contained + assertEquals(0, results.size()); + + results = GeoPipeline + .startWithinSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(15.0, 16.0, 56.0, 57.0))) + .toSpatialDatabaseRecordList(); + + assertEquals(1, results.size()); + tx.commit(); + } + } + + @Test + public void testSnapToLine() { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer layer = spatial.getOrCreateEditableLayer(tx, "roads"); + Coordinate crossing_bygg_forstadsgatan = new Coordinate(13.0171471, 55.6074148); + Coordinate[] waypoints_forstadsgatan = {new Coordinate(13.0201511, 55.6066846), + crossing_bygg_forstadsgatan}; + LineString ostra_forstadsgatan_malmo = layer.getGeometryFactory().createLineString(waypoints_forstadsgatan); + Coordinate[] waypoints_byggmastaregatan = {crossing_bygg_forstadsgatan, + new Coordinate(13.0182092, 55.6088238)}; + LineString byggmastaregatan_malmo = layer.getGeometryFactory().createLineString(waypoints_byggmastaregatan); + LineString[] test_way_segments = {byggmastaregatan_malmo, ostra_forstadsgatan_malmo}; + /* MultiLineString test_way = */ + layer.getGeometryFactory().createMultiLineString(test_way_segments); + }); + inTx(tx -> { + // Coordinate slussgatan14 = new Coordinate( 13.0181127, 55.608236 ); + //TODO now determine the nearest point on test_way to slussis + }); + } + + @Test + public void testEditableLayers() { + testSpecificEditableLayer("test dynamic layer with property encoder", SimplePropertyEncoder.class, + DynamicLayer.class); + testSpecificEditableLayer("test dynamic layer with graph encoder", SimpleGraphEncoder.class, + DynamicLayer.class); + testSpecificEditableLayer("test OSM layer with OSM encoder", OSMGeometryEncoder.class, OSMLayer.class); + testSpecificEditableLayer("test editable layer with property encoder", SimplePropertyEncoder.class, + EditableLayerImpl.class); + testSpecificEditableLayer("test editable layer with graph encoder", SimpleGraphEncoder.class, + EditableLayerImpl.class); + testSpecificEditableLayer("test editable layer with OSM encoder", OSMGeometryEncoder.class, + EditableLayerImpl.class); + } + + private String testSpecificEditableLayer(String layerName, Class geometryEncoderClass, + Class layerClass) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + Layer layer = spatial.createLayer(tx, layerName, geometryEncoderClass, layerClass); + assertNotNull(layer); + assertTrue(layer instanceof EditableLayer, "Should be an editable layer"); + }); + inTx(tx -> { + Layer layer = spatial.getLayer(tx, layerName); + assertNotNull(layer); + assertTrue(layer instanceof EditableLayer, "Should be an editable layer"); + EditableLayer editableLayer = (EditableLayer) layer; + + CoordinateList coordinates = new CoordinateList(); + coordinates.add(new Coordinate(13.1, 56.2), false); + coordinates.add(new Coordinate(13.2, 56.0), false); + coordinates.add(new Coordinate(13.3, 56.2), false); + coordinates.add(new Coordinate(13.2, 56.0), false); + coordinates.add(new Coordinate(13.1, 56.2), false); + coordinates.add(new Coordinate(13.0, 56.0), false); + editableLayer.add(tx, layer.getGeometryFactory().createLineString(coordinates.toCoordinateArray())); + + coordinates = new CoordinateList(); + coordinates.add(new Coordinate(14.1, 56.0), false); + coordinates.add(new Coordinate(14.3, 56.1), false); + coordinates.add(new Coordinate(14.2, 56.1), false); + coordinates.add(new Coordinate(14.0, 56.0), false); + editableLayer.add(tx, layer.getGeometryFactory().createLineString(coordinates.toCoordinateArray())); + }); + + // TODO this test is not complete + + try (Transaction tx = graphDb.beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + printResults(layer, GeoPipeline + .startIntersectSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(13.2, 14.1, 56.1, 56.2))) + .toSpatialDatabaseRecordList()); + + printResults(layer, GeoPipeline + .startContainSearch(tx, layer, + layer.getGeometryFactory().toGeometry(new Envelope(12.0, 15.0, 55.0, 57.0))) + .toSpatialDatabaseRecordList()); + tx.commit(); + } + return layerName; + } + + private void printResults(Layer layer, List results) { + System.out.println("\tTesting layer '" + layer.getName() + "' (class " + layer.getClass() + "), found results: " + + results.size()); + for (SpatialDatabaseRecord r : results) { + System.out.println("\t\tGeometry: " + r); + } + } + + @Test + public void testShapefileExport() throws Exception { + ShapefileExporter exporter = new ShapefileExporter(graphDb); + exporter.setExportDir("target/export"); + ArrayList layers = new ArrayList<>(); + + layers.add(testSpecificEditableLayer("test dynamic layer with property encoder", SimplePropertyEncoder.class, + DynamicLayer.class)); + layers.add(testSpecificEditableLayer("test dynamic layer with graph encoder", SimpleGraphEncoder.class, + DynamicLayer.class)); + layers.add(testSpecificEditableLayer("test dynamic layer with OSM encoder", OSMGeometryEncoder.class, + OSMLayer.class)); + + for (String layerName : layers) { + exporter.exportLayer(layerName); + } + } + + @Test + public void testIndexAccessAfterBulkInsertion() { + // Use these two lines if you want to examine the output. // File dbPath = new File("target/var/BulkTest"); // GraphDatabaseService db = new GraphDatabaseFactory().newEmbeddedDatabase(dbPath.getCanonicalPath()); - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - inTx(tx -> spatial.getOrCreateSimplePointLayer(tx, "Coordinates", "rtree", "lat", "lon")); - - Random rand = new Random(); - - try (Transaction tx = graphDb.beginTx()) { - SimplePointLayer layer = (SimplePointLayer) spatial.getLayer(tx, "Coordinates"); - List coordinateNodes = new ArrayList<>(); - for (int i = 0; i < 1000; i++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", rand.nextDouble()); - node.setProperty("lon", rand.nextDouble()); - coordinateNodes.add(node); - } - layer.addAll(tx, coordinateNodes); - tx.commit(); - } - - try (Transaction tx = graphDb.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 - Result result = tx.execute("CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) YIELD node AS malmo"); - int i = 0; - ResourceIterator thing = result.columnAs("malmo"); - while (thing.hasNext()) { - assertNotNull(thing.next()); - i++; - } - assertEquals(i, 1000); - tx.commit(); - } - - try (Transaction tx = graphDb.beginTx()) { - String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + - "MATCH (n)-[:RTREE_CHILD]->(m)-[:RTREE_REFERENCE]->(p)\n" + - "RETURN count(p)"; - Result result = tx.execute(cypher); + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + inTx(tx -> spatial.getOrCreateSimplePointLayer(tx, "Coordinates", "rtree", "lat", "lon")); + + Random rand = new Random(); + + try (Transaction tx = graphDb.beginTx()) { + SimplePointLayer layer = (SimplePointLayer) spatial.getLayer(tx, "Coordinates"); + List coordinateNodes = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", rand.nextDouble()); + node.setProperty("lon", rand.nextDouble()); + coordinateNodes.add(node); + } + layer.addAll(tx, coordinateNodes); + tx.commit(); + } + + try (Transaction tx = graphDb.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 + Result result = tx.execute( + "CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) YIELD node AS malmo"); + int i = 0; + ResourceIterator thing = result.columnAs("malmo"); + while (thing.hasNext()) { + assertNotNull(thing.next()); + i++; + } + assertEquals(i, 1000); + tx.commit(); + } + + try (Transaction tx = graphDb.beginTx()) { + String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + + "MATCH (n)-[:RTREE_CHILD]->(m)-[:RTREE_REFERENCE]->(p)\n" + + "RETURN count(p)"; + Result result = tx.execute(cypher); // System.out.println(result.columns().toString()); - Object obj = result.columnAs("count(p)").next(); - assertTrue(obj instanceof Long); - assertEquals(1000L, (long) ((Long) obj)); - tx.commit(); - } - } - - private void inTx(Consumer txFunction) { - try (Transaction tx = graphDb.beginTx()) { - txFunction.accept(tx); - tx.commit(); - } - } + Object obj = result.columnAs("count(p)").next(); + assertTrue(obj instanceof Long); + assertEquals(1000L, (long) ((Long) obj)); + tx.commit(); + } + } + + private void inTx(Consumer txFunction) { + try (Transaction tx = graphDb.beginTx()) { + txFunction.accept(tx); + tx.commit(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/Neo4jSpatialDataStoreTest.java b/src/test/java/org/neo4j/gis/spatial/Neo4jSpatialDataStoreTest.java index 27e7145f0..7fb519011 100644 --- a/src/test/java/org/neo4j/gis/spatial/Neo4jSpatialDataStoreTest.java +++ b/src/test/java/org/neo4j/gis/spatial/Neo4jSpatialDataStoreTest.java @@ -1,10 +1,21 @@ package org.neo4j.gis.spatial; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; import org.geotools.api.data.ResourceInfo; +import org.geotools.api.data.SimpleFeatureSource; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.data.neo4j.Neo4jSpatialDataStore; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; -import org.geotools.api.data.SimpleFeatureSource; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.hamcrest.MatcherAssert; @@ -15,112 +26,109 @@ import org.neo4j.gis.spatial.osm.OSMImporter; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.api.feature.simple.SimpleFeatureType; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.HashSet; -import java.util.Set; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; public class Neo4jSpatialDataStoreTest { - private DatabaseManagementService databases; - public GraphDatabaseService graph; + private DatabaseManagementService databases; + public GraphDatabaseService graph; - @BeforeEach - public void setup() throws Exception { - this.databases = new TestDatabaseManagementServiceBuilder(Path.of("target", "test")).impermanent().build(); - this.graph = databases.database(DEFAULT_DATABASE_NAME); - OSMImporter importer = new OSMImporter("map", new ConsoleListener()); - importer.setCharset(StandardCharsets.UTF_8); - importer.setVerbose(false); - importer.importFile(graph, "map.osm"); - importer.reIndex(graph); - } + @BeforeEach + public void setup() throws Exception { + this.databases = new TestDatabaseManagementServiceBuilder(Path.of("target", "test")).impermanent().build(); + this.graph = databases.database(DEFAULT_DATABASE_NAME); + OSMImporter importer = new OSMImporter("map", new ConsoleListener()); + importer.setCharset(StandardCharsets.UTF_8); + importer.setVerbose(false); + importer.importFile(graph, "map.osm"); + importer.reIndex(graph); + } - @AfterEach - public void teardown() { - if (this.databases != null) { - this.databases.shutdown(); - this.databases = null; - this.graph = null; - } - } + @AfterEach + public void teardown() { + if (this.databases != null) { + this.databases.shutdown(); + this.databases = null; + this.graph = null; + } + } - @Test - public void shouldOpenDataStore() { - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); - ReferencedEnvelope bounds = store.getBounds("map"); - MatcherAssert.assertThat(bounds, equalTo(new ReferencedEnvelope(12.7856667, 13.2873561, 55.9254241, 56.2179056, DefaultGeographicCRS.WGS84))); - } + @Test + public void shouldOpenDataStore() { + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); + ReferencedEnvelope bounds = store.getBounds("map"); + MatcherAssert.assertThat(bounds, equalTo(new ReferencedEnvelope(12.7856667, 13.2873561, 55.9254241, 56.2179056, + DefaultGeographicCRS.WGS84))); + } - @Test - public void shouldOpenDataStoreOnNonSpatialDatabase() { - DatabaseManagementService otherDatabases = null; - try { - otherDatabases = new TestDatabaseManagementServiceBuilder(Path.of("target", "other-db")).impermanent().build(); - GraphDatabaseService otherGraph = otherDatabases.database(DEFAULT_DATABASE_NAME); - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(otherGraph); - ReferencedEnvelope bounds = store.getBounds("map"); - // TODO: rather should throw a descriptive exception - MatcherAssert.assertThat(bounds, equalTo(null)); - } finally { - if (otherDatabases != null) - otherDatabases.shutdown(); - } - } + @Test + public void shouldOpenDataStoreOnNonSpatialDatabase() { + DatabaseManagementService otherDatabases = null; + try { + otherDatabases = new TestDatabaseManagementServiceBuilder(Path.of("target", "other-db")).impermanent() + .build(); + GraphDatabaseService otherGraph = otherDatabases.database(DEFAULT_DATABASE_NAME); + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(otherGraph); + ReferencedEnvelope bounds = store.getBounds("map"); + // TODO: rather should throw a descriptive exception + MatcherAssert.assertThat(bounds, equalTo(null)); + } finally { + if (otherDatabases != null) { + otherDatabases.shutdown(); + } + } + } - @Test - public void shouldBeAbleToListLayers() throws IOException { - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); - String[] layers = store.getTypeNames(); - MatcherAssert.assertThat("Expected one layer", layers.length, equalTo(1)); - MatcherAssert.assertThat(layers[0], equalTo("map")); - } + @Test + public void shouldBeAbleToListLayers() throws IOException { + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); + String[] layers = store.getTypeNames(); + MatcherAssert.assertThat("Expected one layer", layers.length, equalTo(1)); + MatcherAssert.assertThat(layers[0], equalTo("map")); + } - @Test - public void shouldBeAbleToGetSchemaForLayer() throws IOException { - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); - SimpleFeatureType schema = store.getSchema("map"); - MatcherAssert.assertThat("Expected 25 attributes", schema.getAttributeCount(), equalTo(25)); - MatcherAssert.assertThat("Expected geometry attribute to be called 'the_geom'", schema.getAttributeDescriptors().get(0).getLocalName(), equalTo("the_geom")); - } + @Test + public void shouldBeAbleToGetSchemaForLayer() throws IOException { + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); + SimpleFeatureType schema = store.getSchema("map"); + MatcherAssert.assertThat("Expected 25 attributes", schema.getAttributeCount(), equalTo(25)); + MatcherAssert.assertThat("Expected geometry attribute to be called 'the_geom'", + schema.getAttributeDescriptors().get(0).getLocalName(), equalTo("the_geom")); + } - @Test - public void shouldBeAbleToGetFeatureSourceForLayer() throws IOException { - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); - SimpleFeatureSource source = store.getFeatureSource("map"); - SimpleFeatureCollection features = source.getFeatures(); - MatcherAssert.assertThat("Expected 217 features", features.size(), equalTo(217)); - MatcherAssert.assertThat("Expected there to be a feature with name 'Nybrodalsvägen'", featureNames(features), hasItem("Nybrodalsvägen")); - } + @Test + public void shouldBeAbleToGetFeatureSourceForLayer() throws IOException { + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); + SimpleFeatureSource source = store.getFeatureSource("map"); + SimpleFeatureCollection features = source.getFeatures(); + MatcherAssert.assertThat("Expected 217 features", features.size(), equalTo(217)); + MatcherAssert.assertThat("Expected there to be a feature with name 'Nybrodalsvägen'", featureNames(features), + hasItem("Nybrodalsvägen")); + } - @Test - public void shouldBeAbleToGetInfoForLayer() throws IOException { - Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); - SimpleFeatureSource source = store.getFeatureSource("map"); - ResourceInfo info = source.getInfo(); - ReferencedEnvelope bounds = info.getBounds(); - MatcherAssert.assertThat(bounds, equalTo(new ReferencedEnvelope(12.7856667, 13.2873561, 55.9254241, 56.2179056, DefaultGeographicCRS.WGS84))); - SimpleFeatureCollection features = source.getFeatures(); - MatcherAssert.assertThat("Expected 217 features", features.size(), equalTo(217)); - MatcherAssert.assertThat("Expected there to be a feature with name 'Nybrodalsvägen'", featureNames(features), hasItem("Nybrodalsvägen")); - } + @Test + public void shouldBeAbleToGetInfoForLayer() throws IOException { + Neo4jSpatialDataStore store = new Neo4jSpatialDataStore(graph); + SimpleFeatureSource source = store.getFeatureSource("map"); + ResourceInfo info = source.getInfo(); + ReferencedEnvelope bounds = info.getBounds(); + MatcherAssert.assertThat(bounds, equalTo(new ReferencedEnvelope(12.7856667, 13.2873561, 55.9254241, 56.2179056, + DefaultGeographicCRS.WGS84))); + SimpleFeatureCollection features = source.getFeatures(); + MatcherAssert.assertThat("Expected 217 features", features.size(), equalTo(217)); + MatcherAssert.assertThat("Expected there to be a feature with name 'Nybrodalsvägen'", featureNames(features), + hasItem("Nybrodalsvägen")); + } - private Set featureNames(SimpleFeatureCollection features) { - HashSet names = new HashSet<>(); - SimpleFeatureIterator featureIterator = features.features(); - while (featureIterator.hasNext()) { - SimpleFeature feature = featureIterator.next(); - Object name = feature.getAttribute("name"); - if (name != null) names.add(name.toString()); - } - return names; - } + private Set featureNames(SimpleFeatureCollection features) { + HashSet names = new HashSet<>(); + SimpleFeatureIterator featureIterator = features.features(); + while (featureIterator.hasNext()) { + SimpleFeature feature = featureIterator.next(); + Object name = feature.getAttribute("name"); + if (name != null) { + names.add(name.toString()); + } + } + return names; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/Neo4jTestCase.java b/src/test/java/org/neo4j/gis/spatial/Neo4jTestCase.java index 53ed95a59..6222891d2 100644 --- a/src/test/java/org/neo4j/gis/spatial/Neo4jTestCase.java +++ b/src/test/java/org/neo4j/gis/spatial/Neo4jTestCase.java @@ -19,6 +19,14 @@ */ package org.neo4j.gis.spatial; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -29,160 +37,153 @@ import org.neo4j.gis.spatial.procedures.SpatialProcedures; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.config.Setting; +import org.neo4j.io.fs.EphemeralFileSystemAbstraction; import org.neo4j.io.fs.FileUtils; import org.neo4j.kernel.api.procedure.GlobalProcedures; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import org.neo4j.io.fs.EphemeralFileSystemAbstraction; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; /** * Base class for the meta model tests. */ public abstract class Neo4jTestCase { - static final Map, Object> NORMAL_CONFIG = new HashMap<>(); - - static { - //NORMAL_CONFIG.put( GraphDatabaseSettings.nodestore_mapped_memory_size.name(), "50M" ); - //NORMAL_CONFIG.put( GraphDatabaseSettings.relationshipstore_mapped_memory_size.name(), "120M" ); - //NORMAL_CONFIG.put( GraphDatabaseSettings.nodestore_propertystore_mapped_memory_size.name(), "150M" ); - //NORMAL_CONFIG.put( GraphDatabaseSettings.strings_mapped_memory_size.name(), "200M" ); - //NORMAL_CONFIG.put( GraphDatabaseSettings.arrays_mapped_memory_size.name(), "0M" ); - NORMAL_CONFIG.put(GraphDatabaseSettings.pagecache_memory, 200000000l); - NORMAL_CONFIG.put(GraphDatabaseInternalSettings.trace_cursors, true); - } - - static final Map, Object> LARGE_CONFIG = new HashMap<>(); - - static { - //LARGE_CONFIG.put( GraphDatabaseSettings.nodestore_mapped_memory_size.name(), "100M" ); - //LARGE_CONFIG.put( GraphDatabaseSettings.relationshipstore_mapped_memory_size.name(), "300M" ); - //LARGE_CONFIG.put( GraphDatabaseSettings.nodestore_propertystore_mapped_memory_size.name(), "400M" ); - //LARGE_CONFIG.put( GraphDatabaseSettings.strings_mapped_memory_size.name(), "800M" ); - //LARGE_CONFIG.put( GraphDatabaseSettings.arrays_mapped_memory_size.name(), "10M" ); - LARGE_CONFIG.put(GraphDatabaseSettings.pagecache_memory, 100000000l); - } - - private static final File basePath = new File("target/var"); - private static final Path dbPath = new File(basePath, "neo4j-db").toPath(); - private DatabaseManagementService databases; - private GraphDatabaseService graphDb; - - private long storePrefix; - - @BeforeEach - public void setUp() throws Exception { - updateStorePrefix(); - setUp(true); - } - - private void updateStorePrefix() { - storePrefix++; - } - - /** - * Configurable options for text cases, with or without deleting the previous database, and with - * or without using the BatchInserter for higher creation speeds. Note that tests that need to - * delete nodes or use transactions should not use the BatchInserter. - */ - protected void setUp(boolean deleteDb) throws Exception { - shutdownDatabase(deleteDb); - Map, Object> config = NORMAL_CONFIG; - String largeMode = System.getProperty("spatial.test.large"); - if (largeMode != null && largeMode.equalsIgnoreCase("true")) { - config = LARGE_CONFIG; - } - databases = new TestDatabaseManagementServiceBuilder(getDbPath()).setConfig(config).build(); - graphDb = databases.database(DEFAULT_DATABASE_NAME); - ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(GlobalProcedures.class).registerProcedure(SpatialProcedures.class); - } - - /** - * For test cases that want to control their own database access, we should - * shutdown the current one. - */ - private void shutdownDatabase(boolean deleteDb) { - beforeShutdown(); - if (graphDb != null) { - databases.shutdown(); - databases = null; - graphDb = null; - } - if (deleteDb) { - deleteDatabase(); - } - } - - private static EphemeralFileSystemAbstraction fileSystem; - - @BeforeAll - static void beforeAll() throws IOException { - fileSystem = new EphemeralFileSystemAbstraction(); - fileSystem.mkdirs(new File("target").toPath()); - } - - @AfterAll - static void afterAll() throws IOException { - fileSystem.close(); - } - - @AfterEach - public void tearDown() { - shutdownDatabase(true); - } - - private void beforeShutdown() { - } - - Path getNeoPath() { - return dbPath.toAbsolutePath(); - } - - Path getDbPath() { - return dbPath.toAbsolutePath().resolve("test-" + storePrefix); - } - - private void deleteDatabase() { - try { - FileUtils.deleteDirectory(getNeoPath()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - static void deleteBaseDir() { - deleteFileOrDirectory(basePath); - } - - private static void deleteFileOrDirectory(File file) { - if (!file.exists()) { - return; - } - - if (file.isDirectory()) { - for (File child : Objects.requireNonNull(file.listFiles())) { - deleteFileOrDirectory(child); - } - } else { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } - } - - void printDatabaseStats() { - Neo4jTestUtils.printDatabaseStats(graphDb(), getDbPath().toFile()); - } - - protected GraphDatabaseService graphDb() { - return graphDb; - } + + static final Map, Object> NORMAL_CONFIG = new HashMap<>(); + + static { + //NORMAL_CONFIG.put( GraphDatabaseSettings.nodestore_mapped_memory_size.name(), "50M" ); + //NORMAL_CONFIG.put( GraphDatabaseSettings.relationshipstore_mapped_memory_size.name(), "120M" ); + //NORMAL_CONFIG.put( GraphDatabaseSettings.nodestore_propertystore_mapped_memory_size.name(), "150M" ); + //NORMAL_CONFIG.put( GraphDatabaseSettings.strings_mapped_memory_size.name(), "200M" ); + //NORMAL_CONFIG.put( GraphDatabaseSettings.arrays_mapped_memory_size.name(), "0M" ); + NORMAL_CONFIG.put(GraphDatabaseSettings.pagecache_memory, 200000000l); + NORMAL_CONFIG.put(GraphDatabaseInternalSettings.trace_cursors, true); + } + + static final Map, Object> LARGE_CONFIG = new HashMap<>(); + + static { + //LARGE_CONFIG.put( GraphDatabaseSettings.nodestore_mapped_memory_size.name(), "100M" ); + //LARGE_CONFIG.put( GraphDatabaseSettings.relationshipstore_mapped_memory_size.name(), "300M" ); + //LARGE_CONFIG.put( GraphDatabaseSettings.nodestore_propertystore_mapped_memory_size.name(), "400M" ); + //LARGE_CONFIG.put( GraphDatabaseSettings.strings_mapped_memory_size.name(), "800M" ); + //LARGE_CONFIG.put( GraphDatabaseSettings.arrays_mapped_memory_size.name(), "10M" ); + LARGE_CONFIG.put(GraphDatabaseSettings.pagecache_memory, 100000000l); + } + + private static final File basePath = new File("target/var"); + private static final Path dbPath = new File(basePath, "neo4j-db").toPath(); + private DatabaseManagementService databases; + private GraphDatabaseService graphDb; + + private long storePrefix; + + @BeforeEach + public void setUp() throws Exception { + updateStorePrefix(); + setUp(true); + } + + private void updateStorePrefix() { + storePrefix++; + } + + /** + * Configurable options for text cases, with or without deleting the previous database, and with + * or without using the BatchInserter for higher creation speeds. Note that tests that need to + * delete nodes or use transactions should not use the BatchInserter. + */ + protected void setUp(boolean deleteDb) throws Exception { + shutdownDatabase(deleteDb); + Map, Object> config = NORMAL_CONFIG; + String largeMode = System.getProperty("spatial.test.large"); + if (largeMode != null && largeMode.equalsIgnoreCase("true")) { + config = LARGE_CONFIG; + } + databases = new TestDatabaseManagementServiceBuilder(getDbPath()).setConfig(config).build(); + graphDb = databases.database(DEFAULT_DATABASE_NAME); + ((GraphDatabaseAPI) graphDb).getDependencyResolver().resolveDependency(GlobalProcedures.class) + .registerProcedure(SpatialProcedures.class); + } + + /** + * For test cases that want to control their own database access, we should + * shutdown the current one. + */ + private void shutdownDatabase(boolean deleteDb) { + beforeShutdown(); + if (graphDb != null) { + databases.shutdown(); + databases = null; + graphDb = null; + } + if (deleteDb) { + deleteDatabase(); + } + } + + private static EphemeralFileSystemAbstraction fileSystem; + + @BeforeAll + static void beforeAll() throws IOException { + fileSystem = new EphemeralFileSystemAbstraction(); + fileSystem.mkdirs(new File("target").toPath()); + } + + @AfterAll + static void afterAll() throws IOException { + fileSystem.close(); + } + + @AfterEach + public void tearDown() { + shutdownDatabase(true); + } + + private void beforeShutdown() { + } + + Path getNeoPath() { + return dbPath.toAbsolutePath(); + } + + Path getDbPath() { + return dbPath.toAbsolutePath().resolve("test-" + storePrefix); + } + + private void deleteDatabase() { + try { + FileUtils.deleteDirectory(getNeoPath()); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + static void deleteBaseDir() { + deleteFileOrDirectory(basePath); + } + + private static void deleteFileOrDirectory(File file) { + if (!file.exists()) { + return; + } + + if (file.isDirectory()) { + for (File child : Objects.requireNonNull(file.listFiles())) { + deleteFileOrDirectory(child); + } + } else { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } + + void printDatabaseStats() { + Neo4jTestUtils.printDatabaseStats(graphDb(), getDbPath().toFile()); + } + + protected GraphDatabaseService graphDb() { + return graphDb; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/Neo4jTestUtils.java b/src/test/java/org/neo4j/gis/spatial/Neo4jTestUtils.java index 25be10eee..db9cebdce 100644 --- a/src/test/java/org/neo4j/gis/spatial/Neo4jTestUtils.java +++ b/src/test/java/org/neo4j/gis/spatial/Neo4jTestUtils.java @@ -19,156 +19,161 @@ */ package org.neo4j.gis.spatial; -import org.neo4j.gis.spatial.index.IndexManager; -import org.neo4j.gis.spatial.rtree.RTreeIndex; -import org.neo4j.gis.spatial.rtree.RTreeRelationshipTypes; -import org.neo4j.graphdb.*; -import org.neo4j.internal.kernel.api.security.SecurityContext; -import org.neo4j.kernel.internal.GraphDatabaseAPI; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import org.neo4j.gis.spatial.index.IndexManager; +import org.neo4j.gis.spatial.rtree.RTreeIndex; +import org.neo4j.gis.spatial.rtree.RTreeRelationshipTypes; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.security.SecurityContext; +import org.neo4j.kernel.internal.GraphDatabaseAPI; public class Neo4jTestUtils { - public static int countIterable(Iterable iterable) { - int counter = 0; - Iterator itr = iterable.iterator(); - while (itr.hasNext()) { - itr.next(); - counter++; - } - return counter; - } - - public static void debugIndexTree(GraphDatabaseService db, String layerName) { - try (Transaction tx = db.beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); - Layer layer = spatial.getLayer(tx, layerName); - RTreeIndex index = (RTreeIndex) layer.getIndex(); - printTree(index.getIndexRoot(tx), 0); - tx.commit(); - } - - } - - private static String arrayString(double[] test) { - StringBuffer sb = new StringBuffer(); - for (double d : test) { - addToArrayString(sb, d); - } - sb.append("]"); - return sb.toString(); - } - - private static void addToArrayString(StringBuffer sb, Object obj) { - if (sb.length() == 0) { - sb.append("["); - } else { - sb.append(","); - } - sb.append(obj); - } - - public static void printTree(Node root, int depth) { - StringBuffer tab = new StringBuffer(); - for (int i = 0; i < depth; i++) { - tab.append(" "); - } - - if (root.hasProperty(Constants.PROP_BBOX)) { - System.out.println(tab.toString() + "INDEX: " + root + " BBOX[" + arrayString((double[]) root.getProperty(Constants.PROP_BBOX)) + "]"); - } else { - System.out.println(tab.toString() + "INDEX: " + root); - } - - StringBuffer data = new StringBuffer(); - for (Relationship rel : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { - if (data.length() > 0) { - data.append(", "); - } else { - data.append("DATA: "); - } + public static int countIterable(Iterable iterable) { + int counter = 0; + Iterator itr = iterable.iterator(); + while (itr.hasNext()) { + itr.next(); + counter++; + } + return counter; + } + + public static void debugIndexTree(GraphDatabaseService db, String layerName) { + try (Transaction tx = db.beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); + Layer layer = spatial.getLayer(tx, layerName); + RTreeIndex index = (RTreeIndex) layer.getIndex(); + printTree(index.getIndexRoot(tx), 0); + tx.commit(); + } + + } + + private static String arrayString(double[] test) { + StringBuffer sb = new StringBuffer(); + for (double d : test) { + addToArrayString(sb, d); + } + sb.append("]"); + return sb.toString(); + } + + private static void addToArrayString(StringBuffer sb, Object obj) { + if (sb.length() == 0) { + sb.append("["); + } else { + sb.append(","); + } + sb.append(obj); + } + + public static void printTree(Node root, int depth) { + StringBuffer tab = new StringBuffer(); + for (int i = 0; i < depth; i++) { + tab.append(" "); + } + + if (root.hasProperty(Constants.PROP_BBOX)) { + System.out.println(tab.toString() + "INDEX: " + root + " BBOX[" + arrayString( + (double[]) root.getProperty(Constants.PROP_BBOX)) + "]"); + } else { + System.out.println(tab.toString() + "INDEX: " + root); + } + + StringBuffer data = new StringBuffer(); + for (Relationship rel : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_REFERENCE)) { + if (data.length() > 0) { + data.append(", "); + } else { + data.append("DATA: "); + } // data.append(rel.getEndNode().toString()); - data.append(rel.getEndNode().toString() + " BBOX[" + arrayString((double[]) rel.getEndNode().getProperty - (Constants - .PROP_BBOX)) + "]"); - } - - if (data.length() > 0) { - System.out.println(" " + tab + data); - } - - for (Relationship rel : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - printTree(rel.getEndNode(), depth + 1); - } - } - - public static void assertCollection(Collection collection, T... expectedItems) { - String collectionString = join(", ", collection.toArray()); - assertEquals(collectionString, expectedItems.length, collection.size()); - for (T item : expectedItems) { - assertTrue(collection.contains(item)); - } - } - - public static Collection asCollection(Iterable iterable) { - List list = new ArrayList(); - for (T item : iterable) { - list.add(item); - } - return list; - } - - private static String join(String delimiter, T... items) { - StringBuffer buffer = new StringBuffer(); - for (T item : items) { - if (buffer.length() > 0) { - buffer.append(delimiter); - } - buffer.append(item.toString()); - } - return buffer.toString(); - } - - public static void printDatabaseStats(GraphDatabaseService db, File path) { - System.out.println("Database stats:"); - System.out.println("\tTotal disk usage: " + (databaseDiskUsage(path)) / (1024.0 * 1024.0) + "MB"); - System.out.println("\tTotal # nodes: " + getNumberOfNodes(db)); - System.out.println("\tTotal # rels: " + getNumberOfRelationships(db)); - } - - private static long calculateDiskUsage(File file) { - if (file.isDirectory()) { - long count = 0; - for (File sub : file.listFiles()) { - count += calculateDiskUsage(sub); - } - return count; - } else { - return file.length(); - } - } - - private static long databaseDiskUsage(File path) { - return calculateDiskUsage(path); - } - - private static long getNumberOfNodes(GraphDatabaseService db) { - try (Transaction tx = db.beginTx()) { - return (Long) tx.execute("MATCH (n) RETURN count(n)").columnAs("count(n)").next(); - } - } - - private static long getNumberOfRelationships(GraphDatabaseService db) { - try (Transaction tx = db.beginTx()) { - return (Long) tx.execute("MATCH ()-[r]->() RETURN count(r)").columnAs("count(r)").next(); - } - } + data.append(rel.getEndNode().toString() + " BBOX[" + arrayString((double[]) rel.getEndNode().getProperty + (Constants + .PROP_BBOX)) + "]"); + } + + if (data.length() > 0) { + System.out.println(" " + tab + data); + } + + for (Relationship rel : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + printTree(rel.getEndNode(), depth + 1); + } + } + + public static void assertCollection(Collection collection, T... expectedItems) { + String collectionString = join(", ", collection.toArray()); + assertEquals(collectionString, expectedItems.length, collection.size()); + for (T item : expectedItems) { + assertTrue(collection.contains(item)); + } + } + + public static Collection asCollection(Iterable iterable) { + List list = new ArrayList(); + for (T item : iterable) { + list.add(item); + } + return list; + } + + private static String join(String delimiter, T... items) { + StringBuffer buffer = new StringBuffer(); + for (T item : items) { + if (buffer.length() > 0) { + buffer.append(delimiter); + } + buffer.append(item.toString()); + } + return buffer.toString(); + } + + public static void printDatabaseStats(GraphDatabaseService db, File path) { + System.out.println("Database stats:"); + System.out.println("\tTotal disk usage: " + (databaseDiskUsage(path)) / (1024.0 * 1024.0) + "MB"); + System.out.println("\tTotal # nodes: " + getNumberOfNodes(db)); + System.out.println("\tTotal # rels: " + getNumberOfRelationships(db)); + } + + private static long calculateDiskUsage(File file) { + if (file.isDirectory()) { + long count = 0; + for (File sub : file.listFiles()) { + count += calculateDiskUsage(sub); + } + return count; + } else { + return file.length(); + } + } + + private static long databaseDiskUsage(File path) { + return calculateDiskUsage(path); + } + + private static long getNumberOfNodes(GraphDatabaseService db) { + try (Transaction tx = db.beginTx()) { + return (Long) tx.execute("MATCH (n) RETURN count(n)").columnAs("count(n)").next(); + } + } + + private static long getNumberOfRelationships(GraphDatabaseService db) { + try (Transaction tx = db.beginTx()) { + return (Long) tx.execute("MATCH ()-[r]->() RETURN count(r)").columnAs("count(r)").next(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/OsmAnalysisTest.java b/src/test/java/org/neo4j/gis/spatial/OsmAnalysisTest.java index e1ee17644..5ae337908 100644 --- a/src/test/java/org/neo4j/gis/spatial/OsmAnalysisTest.java +++ b/src/test/java/org/neo4j/gis/spatial/OsmAnalysisTest.java @@ -19,6 +19,25 @@ */ package org.neo4j.gis.spatial; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.data.neo4j.StyledImageExporter; import org.geotools.geometry.jts.ReferencedEnvelope; import org.junit.jupiter.params.ParameterizedTest; @@ -39,395 +58,411 @@ import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.io.File; -import java.io.IOException; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Stream; public class OsmAnalysisTest extends TestOSMImportBase { - public static final String spatialTestMode = System.getProperty("spatial.test.mode"); - public static final boolean usePoints = true; - private static Stream parameters() { - deleteBaseDir(); - String[] smallModels = new String[]{"one-street.osm", "two-street.osm"}; - //String[] mediumModels = new String[]{"map.osm", "map2.osm"}; - String[] largeModels = new String[]{"cyprus.osm", "croatia.osm", "denmark.osm"}; + public static final String spatialTestMode = System.getProperty("spatial.test.mode"); + public static final boolean usePoints = true; + + private static Stream parameters() { + deleteBaseDir(); + String[] smallModels = new String[]{"one-street.osm", "two-street.osm"}; + //String[] mediumModels = new String[]{"map.osm", "map2.osm"}; + String[] largeModels = new String[]{"cyprus.osm", "croatia.osm", "denmark.osm"}; - // Setup default test cases (short or medium only, no long cases) - ArrayList layersToTest = new ArrayList<>(Arrays.asList(smallModels)); + // Setup default test cases (short or medium only, no long cases) + ArrayList layersToTest = new ArrayList<>(Arrays.asList(smallModels)); // layersToTest.addAll(Arrays.asList(mediumModels)); - // Now modify the test cases based on the spatial.test.mode setting - if (spatialTestMode != null && spatialTestMode.equals("long")) { - // Very long running tests - layersToTest.addAll(Arrays.asList(largeModels)); - } else if (spatialTestMode != null && spatialTestMode.equals("short")) { - // Tests used for a quick check - layersToTest.clear(); - layersToTest.addAll(Arrays.asList(smallModels)); - } else if (spatialTestMode != null && spatialTestMode.equals("dev")) { - // Tests relevant to current development - layersToTest.clear(); - // layersToTest.add("/home/craig/Desktop/AWE/Data/MapData/baden-wurttemberg.osm/baden-wurttemberg.osm"); - // layersToTest.add("cyprus.osm"); - // layersToTest.add("croatia.osm"); - layersToTest.add("cyprus.osm"); - } - - int[] years = new int[]{3}; - int[] days = new int[]{1}; - - // Finally build the set of complete test cases based on the collection above - ArrayList params = new ArrayList<>(); - for (final String layerName : layersToTest) { - for (final int y : years) { - for (final int d : days) { - params.add(Arguments.of(layerName, y, d)); - } - } - } - System.out.println("This suite has " + params.size() + " tests"); - for (Arguments arguments : params) { - System.out.println("\t" + Arrays.toString(arguments.get())); - } - return params.stream(); - } - - @ParameterizedTest - @MethodSource("parameters") - public void runTest(String layerName, int years, int days) throws Exception { - runAnalysis(layerName, years, days); - } - - protected void runAnalysis(String osm, int years, int days) throws Exception { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - boolean alreadyImported; - try (Transaction tx = graphDb().beginTx()) { - alreadyImported = spatial.getLayer(tx, osm) != null; - tx.commit(); - } - if (!alreadyImported) { - runImport(osm, usePoints); - } - testAnalysis2(osm, years, days); - } - - public void testAnalysis2(String osm, int years, int days) throws IOException { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - LinkedHashMap slides = new LinkedHashMap<>(); - Map userIndex = new HashMap<>(); - int user_rank = 1; - long latestTimestamp = 0L; - long firstTimestamp = Long.MAX_VALUE; - try (Transaction tx = graphDb().beginTx()) { - OSMLayer layer = (OSMLayer) spatial.getLayer(tx, osm); - OSMDataset dataset = OSMDataset.fromLayer(tx, layer); - - for (Node cNode : dataset.getAllChangesetNodes(tx)) { - long timestamp = (Long) cNode.getProperty("timestamp", 0L); - Node userNode = dataset.getUser(cNode); - String name = (String) userNode.getProperty("name"); - - User user = userIndex.get(name); - if (user == null) { - user = new User(userNode.getElementId(), name); - userIndex.put(name, user); - } - user.addChangeset(cNode, timestamp); - if (latestTimestamp < timestamp) - latestTimestamp = timestamp; - if (firstTimestamp > timestamp) - firstTimestamp = timestamp; - } - tx.commit(); - } - SortedSet topTen = getTopTen(userIndex); - try (Transaction tx = graphDb().beginTx()) { - OSMLayer layer = (OSMLayer) spatial.getLayer(tx, osm); - Date latest = new Date(latestTimestamp); - Calendar time = Calendar.getInstance(); - time.setTime(latest); - int slidesPerYear = 360 / days; - int slideCount = slidesPerYear * years; - long msPerSlide = (long) days * 24 * 3600000; - int timeWindow = 15; - StringBuilder userQuery = new StringBuilder(); - for (User user : topTen) { - if (userQuery.length() > 0) - userQuery.append(" or "); - userQuery.append("user = '").append(user.name).append("'"); - user_rank++; - } - for (int i = -timeWindow; i < slideCount; i++) { - long timestamp = latestTimestamp - i * msPerSlide; - long maxTime = timestamp + 15 * msPerSlide; - time.setTimeInMillis(timestamp); - Date date = new Date(timestamp); - System.out.println("Preparing slides for " + date); - String name = osm + "-" + date; - DynamicLayerConfig config = layer.addLayerConfig(tx, name, Constants.GTYPE_GEOMETRY, "timestamp > " + timestamp + " and timestamp < " - + maxTime + " and (" + userQuery + ")"); - System.out.println("Added dynamic layer '" + config.getName() + "' with CQL: " + config.getQuery()); - slides.put(config, timestamp); - } - DynamicLayerConfig config = layer.addLayerConfig(tx, osm + "-top-ten", Constants.GTYPE_GEOMETRY, userQuery.toString()); - System.out.println("Added dynamic layer '" + config.getName() + "' with CQL: " + config.getQuery()); - slides.clear(); - slides.put(config, 0L); - tx.commit(); - } - - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - String exportDir = "target/export/" + osm + "/analysis"; - imageExporter.setExportDir(exportDir); - imageExporter.setZoom(2.0); - imageExporter.setOffset(-0.2, 0.25); - imageExporter.setSize(1280, 800); - imageExporter.setStyleFiles(new String[]{"sld/background.sld", "sld/rank.sld"}); - - String[] layerPropertyNames = new String[]{"name", "timestamp", "user", "days", "user_rank"}; - StringBuilder userParams = new StringBuilder(); - user_rank = 1; - for (User user : topTen) { - if (userParams.length() > 0) userParams.append(","); - userParams.append(user.name).append(":").append(user_rank); - user_rank++; - } - - boolean checkedOne = false; - - for (DynamicLayerConfig layerToExport : slides.keySet()) { - try (Transaction tx = graphDb().beginTx()) { - layerToExport.setExtraPropertyNames(tx, layerPropertyNames); - layerToExport.getPropertyMappingManager().addPropertyMapper(tx, "timestamp", "days", "Days", Long.toString(slides.get(layerToExport))); - layerToExport.getPropertyMappingManager().addPropertyMapper(tx, "user", "user_rank", "Map", userParams.toString()); - if (!checkedOne) { - int i = 0; - System.out.println("Checking layer '" + layerToExport + "' in detail"); - SearchRecords records = layerToExport.getIndex().search(tx, new SearchAll()); - for (SpatialRecord record : records) { - System.out.println("Got record " + i + ": " + record); - for (String name : record.getPropertyNames(tx)) { - System.out.println("\t" + name + ":\t" + record.getProperty(tx, name)); - checkedOne = true; - } - if (i++ > 10) - break; - } - } - tx.commit(); - } - - imageExporter.saveLayerImage(new String[]{osm, layerToExport.getName()}, new File(layerToExport.getName() + ".png")); - //break; - } - } - - public void testAnalysis(String osm) throws Exception { - SortedMap layers; - ReferencedEnvelope bbox; - try (Transaction tx = graphDb().beginTx()) { - Node osmImport = tx.findNode(OSMImporter.LABEL_DATASET, "name", osm); - Node usersNode = osmImport.getSingleRelationship(OSMRelation.USERS, Direction.OUTGOING).getEndNode(); - - Map userIndex = collectUserChangesetData(usersNode); - SortedSet topTen = getTopTen(userIndex); - - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - layers = exportPoints(tx, osm, spatial, topTen); - - layers = removeEmptyLayers(tx, layers); - bbox = getEnvelope(tx, layers.values()); - tx.commit(); - } - - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - String exportDir = "target/export/" + osm + "/analysis"; - imageExporter.setExportDir(exportDir); - imageExporter.setZoom(2.0); - imageExporter.setOffset(-0.05, -0.05); - imageExporter.setSize(1280, 800); - - for (String layerName : layers.keySet()) { - SortedMap layersSubset = new TreeMap<>(layers.headMap(layerName)); - - String[] to_render = new String[Math.min(10, layersSubset.size() + 1)]; - to_render[0] = layerName; - if (layersSubset.size() > 0) { - for (int i = 1; i < to_render.length; i++) { - String name = layersSubset.lastKey(); - layersSubset.remove(name); - to_render[i] = name; - } - } - - System.out.println("exporting " + layerName); - imageExporter.saveLayerImage( - to_render, // (String[]) - // layersSubset.keySet().toArray(new - // String[] {}), - "/Users/davidesavazzi/Desktop/amanzi/awe trial/osm_germany/germany_poi_small.sld", - new File(layerName + ".png"), bbox); - } - } - - private ReferencedEnvelope getEnvelope(Transaction tx, Collection layers) { - CoordinateReferenceSystem crs = null; - - Envelope envelope = null; - for (Layer layer : layers) { - Envelope bbox = layer.getIndex().getBoundingBox(tx); - if (envelope == null) { - envelope = new Envelope(bbox); - } else { - envelope.expandToInclude(bbox); - } - if (crs == null) { - crs = layer.getCoordinateReferenceSystem(tx); - } - } - - return new ReferencedEnvelope(Utilities.fromNeo4jToJts(envelope), crs); - } - - private SortedMap removeEmptyLayers(Transaction tx, Map layers) { - SortedMap result = new TreeMap<>(); - - for (Entry entry : layers.entrySet()) { - if (entry.getValue().getIndex().count(tx) > 0) { - result.put(entry.getKey(), entry.getValue()); - } - } - - return result; - } - - private SortedMap exportPoints(Transaction tx, String layerPrefix, SpatialDatabaseService spatialService, Set users) { - SortedMap layers = new TreeMap<>(); - int startYear = 2009; - int endYear = 2011; - - for (int y = startYear; y <= endYear; y++) { - for (int w = 1; w <= 52; w++) { - if (y == 2011 && w == 36) { - break; - } - - String name = layerPrefix + "-" + y + "_"; - if (w >= 10) - name += w; - else - name += "0" + w; - - EditableLayerImpl layer = (EditableLayerImpl) spatialService.createLayer(tx, name, WKBGeometryEncoder.class, EditableLayerImpl.class); - layer.setExtraPropertyNames(new String[]{"user_id", "user_name", "year", "month", "dayOfMonth", "weekOfYear"}, tx); - - layers.put(name, layer); - } - } - - for (User user : users) { - Node userNode = tx.getNodeByElementId(user.id); - System.out.println("analyzing user: " + userNode.getProperty("name")); - for (Relationship r : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { - Node changeset = r.getStartNode(); - if (changeset.hasProperty("changeset")) { - System.out.println("analyzing changeset: " + changeset.getProperty("changeset")); - for (Relationship nr : changeset.getRelationships(Direction.INCOMING, OSMRelation.CHANGESET)) { - Node changedNode = nr.getStartNode(); - if (changedNode.hasProperty("node_osm_id") && changedNode.hasProperty("timestamp")) { - long timestamp = (Long) changedNode.getProperty("timestamp"); - - Calendar c = Calendar.getInstance(); - c.setTimeInMillis(timestamp); - int nodeYear = c.get(Calendar.YEAR); - int nodeWeek = c.get(Calendar.WEEK_OF_YEAR); - - if (layers.containsKey(layerPrefix + "-" + nodeYear + "_" + nodeWeek)) { - EditableLayer l = (EditableLayer) layers.get(layerPrefix + "-" + nodeYear + "_" + nodeWeek); - l.add(tx, l.getGeometryFactory().createPoint( - new Coordinate((Double) changedNode.getProperty("lon"), (Double) changedNode - .getProperty("lat"))), new String[]{"user_id", "user_name", "year", "month", - "dayOfMonth", "weekOfYear"}, - new Object[]{user.internalId, user.name, c.get(Calendar.YEAR), c.get(Calendar.MONTH), - c.get(Calendar.DAY_OF_MONTH), c.get(Calendar.WEEK_OF_YEAR)}); - } - } - } - } - } - } - - return layers; - } - - private SortedSet getTopTen(Map userIndex) { - SortedSet userList = new TreeSet<>(userIndex.values()); - SortedSet topTen = new TreeSet<>(); - - int count = 0; - for (User user : userList) { - if (count < 10) { - topTen.add(user); - user.internalId = count++; - } else { - break; - } - } - - for (User user : topTen) { - System.out.println(user.id + "# " + user.name + " = " + user.changesets.size()); - } - - return topTen; - } - - private Map collectUserChangesetData(Node usersNode) { - Map userIndex = new HashMap<>(); - for (Relationship r : usersNode.getRelationships(Direction.OUTGOING, OSMRelation.OSM_USER)) { - Node userNode = r.getEndNode(); - String name = (String) userNode.getProperty("name"); - - User user = new User(userNode.getElementId(), name); - userIndex.put(name, user); - - for (Relationship ur : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { - Node node = ur.getStartNode(); - if (node.hasProperty("changeset")) { - user.changesets.add(node.getElementId()); - } - } - } - - return userIndex; - } - - static class User implements Comparable { - - String id; - int internalId; - String name; - List changesets = new ArrayList<>(); - long latestTimestamp = 0L; - - public User(String id, String name) { - this.id = id; - this.name = name; - } - - public void addChangeset(Node cNode, long timestamp) { - changesets.add(cNode.getElementId()); - if (latestTimestamp < timestamp) - latestTimestamp = timestamp; - } - - @Override - public int compareTo(User other) { - return -1 * Integer.compare(changesets.size(), other.changesets.size()); - } - } + // Now modify the test cases based on the spatial.test.mode setting + if (spatialTestMode != null && spatialTestMode.equals("long")) { + // Very long running tests + layersToTest.addAll(Arrays.asList(largeModels)); + } else if (spatialTestMode != null && spatialTestMode.equals("short")) { + // Tests used for a quick check + layersToTest.clear(); + layersToTest.addAll(Arrays.asList(smallModels)); + } else if (spatialTestMode != null && spatialTestMode.equals("dev")) { + // Tests relevant to current development + layersToTest.clear(); + // layersToTest.add("/home/craig/Desktop/AWE/Data/MapData/baden-wurttemberg.osm/baden-wurttemberg.osm"); + // layersToTest.add("cyprus.osm"); + // layersToTest.add("croatia.osm"); + layersToTest.add("cyprus.osm"); + } + + int[] years = new int[]{3}; + int[] days = new int[]{1}; + + // Finally build the set of complete test cases based on the collection above + ArrayList params = new ArrayList<>(); + for (final String layerName : layersToTest) { + for (final int y : years) { + for (final int d : days) { + params.add(Arguments.of(layerName, y, d)); + } + } + } + System.out.println("This suite has " + params.size() + " tests"); + for (Arguments arguments : params) { + System.out.println("\t" + Arrays.toString(arguments.get())); + } + return params.stream(); + } + + @ParameterizedTest + @MethodSource("parameters") + public void runTest(String layerName, int years, int days) throws Exception { + runAnalysis(layerName, years, days); + } + + protected void runAnalysis(String osm, int years, int days) throws Exception { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + boolean alreadyImported; + try (Transaction tx = graphDb().beginTx()) { + alreadyImported = spatial.getLayer(tx, osm) != null; + tx.commit(); + } + if (!alreadyImported) { + runImport(osm, usePoints); + } + testAnalysis2(osm, years, days); + } + + public void testAnalysis2(String osm, int years, int days) throws IOException { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + LinkedHashMap slides = new LinkedHashMap<>(); + Map userIndex = new HashMap<>(); + int user_rank = 1; + long latestTimestamp = 0L; + long firstTimestamp = Long.MAX_VALUE; + try (Transaction tx = graphDb().beginTx()) { + OSMLayer layer = (OSMLayer) spatial.getLayer(tx, osm); + OSMDataset dataset = OSMDataset.fromLayer(tx, layer); + + for (Node cNode : dataset.getAllChangesetNodes(tx)) { + long timestamp = (Long) cNode.getProperty("timestamp", 0L); + Node userNode = dataset.getUser(cNode); + String name = (String) userNode.getProperty("name"); + + User user = userIndex.get(name); + if (user == null) { + user = new User(userNode.getElementId(), name); + userIndex.put(name, user); + } + user.addChangeset(cNode, timestamp); + if (latestTimestamp < timestamp) { + latestTimestamp = timestamp; + } + if (firstTimestamp > timestamp) { + firstTimestamp = timestamp; + } + } + tx.commit(); + } + SortedSet topTen = getTopTen(userIndex); + try (Transaction tx = graphDb().beginTx()) { + OSMLayer layer = (OSMLayer) spatial.getLayer(tx, osm); + Date latest = new Date(latestTimestamp); + Calendar time = Calendar.getInstance(); + time.setTime(latest); + int slidesPerYear = 360 / days; + int slideCount = slidesPerYear * years; + long msPerSlide = (long) days * 24 * 3600000; + int timeWindow = 15; + StringBuilder userQuery = new StringBuilder(); + for (User user : topTen) { + if (userQuery.length() > 0) { + userQuery.append(" or "); + } + userQuery.append("user = '").append(user.name).append("'"); + user_rank++; + } + for (int i = -timeWindow; i < slideCount; i++) { + long timestamp = latestTimestamp - i * msPerSlide; + long maxTime = timestamp + 15 * msPerSlide; + time.setTimeInMillis(timestamp); + Date date = new Date(timestamp); + System.out.println("Preparing slides for " + date); + String name = osm + "-" + date; + DynamicLayerConfig config = layer.addLayerConfig(tx, name, Constants.GTYPE_GEOMETRY, + "timestamp > " + timestamp + " and timestamp < " + + maxTime + " and (" + userQuery + ")"); + System.out.println("Added dynamic layer '" + config.getName() + "' with CQL: " + config.getQuery()); + slides.put(config, timestamp); + } + DynamicLayerConfig config = layer.addLayerConfig(tx, osm + "-top-ten", Constants.GTYPE_GEOMETRY, + userQuery.toString()); + System.out.println("Added dynamic layer '" + config.getName() + "' with CQL: " + config.getQuery()); + slides.clear(); + slides.put(config, 0L); + tx.commit(); + } + + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + String exportDir = "target/export/" + osm + "/analysis"; + imageExporter.setExportDir(exportDir); + imageExporter.setZoom(2.0); + imageExporter.setOffset(-0.2, 0.25); + imageExporter.setSize(1280, 800); + imageExporter.setStyleFiles(new String[]{"sld/background.sld", "sld/rank.sld"}); + + String[] layerPropertyNames = new String[]{"name", "timestamp", "user", "days", "user_rank"}; + StringBuilder userParams = new StringBuilder(); + user_rank = 1; + for (User user : topTen) { + if (userParams.length() > 0) { + userParams.append(","); + } + userParams.append(user.name).append(":").append(user_rank); + user_rank++; + } + + boolean checkedOne = false; + + for (DynamicLayerConfig layerToExport : slides.keySet()) { + try (Transaction tx = graphDb().beginTx()) { + layerToExport.setExtraPropertyNames(tx, layerPropertyNames); + layerToExport.getPropertyMappingManager() + .addPropertyMapper(tx, "timestamp", "days", "Days", Long.toString(slides.get(layerToExport))); + layerToExport.getPropertyMappingManager() + .addPropertyMapper(tx, "user", "user_rank", "Map", userParams.toString()); + if (!checkedOne) { + int i = 0; + System.out.println("Checking layer '" + layerToExport + "' in detail"); + SearchRecords records = layerToExport.getIndex().search(tx, new SearchAll()); + for (SpatialRecord record : records) { + System.out.println("Got record " + i + ": " + record); + for (String name : record.getPropertyNames(tx)) { + System.out.println("\t" + name + ":\t" + record.getProperty(tx, name)); + checkedOne = true; + } + if (i++ > 10) { + break; + } + } + } + tx.commit(); + } + + imageExporter.saveLayerImage(new String[]{osm, layerToExport.getName()}, + new File(layerToExport.getName() + ".png")); + //break; + } + } + + public void testAnalysis(String osm) throws Exception { + SortedMap layers; + ReferencedEnvelope bbox; + try (Transaction tx = graphDb().beginTx()) { + Node osmImport = tx.findNode(OSMImporter.LABEL_DATASET, "name", osm); + Node usersNode = osmImport.getSingleRelationship(OSMRelation.USERS, Direction.OUTGOING).getEndNode(); + + Map userIndex = collectUserChangesetData(usersNode); + SortedSet topTen = getTopTen(userIndex); + + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + layers = exportPoints(tx, osm, spatial, topTen); + + layers = removeEmptyLayers(tx, layers); + bbox = getEnvelope(tx, layers.values()); + tx.commit(); + } + + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + String exportDir = "target/export/" + osm + "/analysis"; + imageExporter.setExportDir(exportDir); + imageExporter.setZoom(2.0); + imageExporter.setOffset(-0.05, -0.05); + imageExporter.setSize(1280, 800); + + for (String layerName : layers.keySet()) { + SortedMap layersSubset = new TreeMap<>(layers.headMap(layerName)); + + String[] to_render = new String[Math.min(10, layersSubset.size() + 1)]; + to_render[0] = layerName; + if (layersSubset.size() > 0) { + for (int i = 1; i < to_render.length; i++) { + String name = layersSubset.lastKey(); + layersSubset.remove(name); + to_render[i] = name; + } + } + + System.out.println("exporting " + layerName); + imageExporter.saveLayerImage( + to_render, // (String[]) + // layersSubset.keySet().toArray(new + // String[] {}), + "/Users/davidesavazzi/Desktop/amanzi/awe trial/osm_germany/germany_poi_small.sld", + new File(layerName + ".png"), bbox); + } + } + + private ReferencedEnvelope getEnvelope(Transaction tx, Collection layers) { + CoordinateReferenceSystem crs = null; + + Envelope envelope = null; + for (Layer layer : layers) { + Envelope bbox = layer.getIndex().getBoundingBox(tx); + if (envelope == null) { + envelope = new Envelope(bbox); + } else { + envelope.expandToInclude(bbox); + } + if (crs == null) { + crs = layer.getCoordinateReferenceSystem(tx); + } + } + + return new ReferencedEnvelope(Utilities.fromNeo4jToJts(envelope), crs); + } + + private SortedMap removeEmptyLayers(Transaction tx, Map layers) { + SortedMap result = new TreeMap<>(); + + for (Entry entry : layers.entrySet()) { + if (entry.getValue().getIndex().count(tx) > 0) { + result.put(entry.getKey(), entry.getValue()); + } + } + + return result; + } + + private SortedMap exportPoints(Transaction tx, String layerPrefix, + SpatialDatabaseService spatialService, Set users) { + SortedMap layers = new TreeMap<>(); + int startYear = 2009; + int endYear = 2011; + + for (int y = startYear; y <= endYear; y++) { + for (int w = 1; w <= 52; w++) { + if (y == 2011 && w == 36) { + break; + } + + String name = layerPrefix + "-" + y + "_"; + if (w >= 10) { + name += w; + } else { + name += "0" + w; + } + + EditableLayerImpl layer = (EditableLayerImpl) spatialService.createLayer(tx, name, + WKBGeometryEncoder.class, EditableLayerImpl.class); + layer.setExtraPropertyNames( + new String[]{"user_id", "user_name", "year", "month", "dayOfMonth", "weekOfYear"}, tx); + + layers.put(name, layer); + } + } + + for (User user : users) { + Node userNode = tx.getNodeByElementId(user.id); + System.out.println("analyzing user: " + userNode.getProperty("name")); + for (Relationship r : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { + Node changeset = r.getStartNode(); + if (changeset.hasProperty("changeset")) { + System.out.println("analyzing changeset: " + changeset.getProperty("changeset")); + for (Relationship nr : changeset.getRelationships(Direction.INCOMING, OSMRelation.CHANGESET)) { + Node changedNode = nr.getStartNode(); + if (changedNode.hasProperty("node_osm_id") && changedNode.hasProperty("timestamp")) { + long timestamp = (Long) changedNode.getProperty("timestamp"); + + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(timestamp); + int nodeYear = c.get(Calendar.YEAR); + int nodeWeek = c.get(Calendar.WEEK_OF_YEAR); + + if (layers.containsKey(layerPrefix + "-" + nodeYear + "_" + nodeWeek)) { + EditableLayer l = (EditableLayer) layers.get( + layerPrefix + "-" + nodeYear + "_" + nodeWeek); + l.add(tx, l.getGeometryFactory().createPoint( + new Coordinate((Double) changedNode.getProperty("lon"), (Double) changedNode + .getProperty("lat"))), + new String[]{"user_id", "user_name", "year", "month", + "dayOfMonth", "weekOfYear"}, + new Object[]{user.internalId, user.name, c.get(Calendar.YEAR), + c.get(Calendar.MONTH), + c.get(Calendar.DAY_OF_MONTH), c.get(Calendar.WEEK_OF_YEAR)}); + } + } + } + } + } + } + + return layers; + } + + private SortedSet getTopTen(Map userIndex) { + SortedSet userList = new TreeSet<>(userIndex.values()); + SortedSet topTen = new TreeSet<>(); + + int count = 0; + for (User user : userList) { + if (count < 10) { + topTen.add(user); + user.internalId = count++; + } else { + break; + } + } + + for (User user : topTen) { + System.out.println(user.id + "# " + user.name + " = " + user.changesets.size()); + } + + return topTen; + } + + private Map collectUserChangesetData(Node usersNode) { + Map userIndex = new HashMap<>(); + for (Relationship r : usersNode.getRelationships(Direction.OUTGOING, OSMRelation.OSM_USER)) { + Node userNode = r.getEndNode(); + String name = (String) userNode.getProperty("name"); + + User user = new User(userNode.getElementId(), name); + userIndex.put(name, user); + + for (Relationship ur : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { + Node node = ur.getStartNode(); + if (node.hasProperty("changeset")) { + user.changesets.add(node.getElementId()); + } + } + } + + return userIndex; + } + + static class User implements Comparable { + + String id; + int internalId; + String name; + List changesets = new ArrayList<>(); + long latestTimestamp = 0L; + + public User(String id, String name) { + this.id = id; + this.name = name; + } + + public void addChangeset(Node cNode, long timestamp) { + changesets.add(cNode.getElementId()); + if (latestTimestamp < timestamp) { + latestTimestamp = timestamp; + } + } + + @Override + public int compareTo(User other) { + return -1 * Integer.compare(changesets.size(), other.changesets.size()); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/ProgressLoggingListenerTest.java b/src/test/java/org/neo4j/gis/spatial/ProgressLoggingListenerTest.java index ee1974011..b6ffd9a51 100644 --- a/src/test/java/org/neo4j/gis/spatial/ProgressLoggingListenerTest.java +++ b/src/test/java/org/neo4j/gis/spatial/ProgressLoggingListenerTest.java @@ -19,49 +19,51 @@ */ package org.neo4j.gis.spatial; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.PrintStream; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.neo4j.gis.spatial.rtree.Listener; import org.neo4j.gis.spatial.rtree.ProgressLoggingListener; -import java.io.PrintStream; - -import static org.mockito.Mockito.*; - public class ProgressLoggingListenerTest { - @Test - public void testProgressLoggingListnerWithAllLogs() { - int unitsOfWork = 10; - long timeWait = 10; - long throttle = 20; - testProgressLoggingListenerWithSpecifiedWaits(unitsOfWork, timeWait, throttle, unitsOfWork + 2); - } + @Test + public void testProgressLoggingListnerWithAllLogs() { + int unitsOfWork = 10; + long timeWait = 10; + long throttle = 20; + testProgressLoggingListenerWithSpecifiedWaits(unitsOfWork, timeWait, throttle, unitsOfWork + 2); + } - @Test - public void testProgressLoggingListnerWithOnlyStartAndEnd() { - int unitsOfWork = 10; - long timeWait = 1000; - long throttle = 10; - testProgressLoggingListenerWithSpecifiedWaits(unitsOfWork, timeWait, throttle, 3); - } + @Test + public void testProgressLoggingListnerWithOnlyStartAndEnd() { + int unitsOfWork = 10; + long timeWait = 1000; + long throttle = 10; + testProgressLoggingListenerWithSpecifiedWaits(unitsOfWork, timeWait, throttle, 3); + } - private void testProgressLoggingListenerWithSpecifiedWaits(int unitsOfWork, long timeWait, long throttle, int expectedLogCount) { - // When running maven-surefire System.out is replaced with a PrintStream that mockito cannot spy on, so we need to wrap it here - PrintStream wrapped = new PrintStream(System.out); - PrintStream out = spy(wrapped); - Listener listener = new ProgressLoggingListener("test", out).setTimeWait(timeWait); - listener.begin(unitsOfWork); - for (int step = 0; step < unitsOfWork; step++) { - listener.worked(1); - try { - Thread.sleep(throttle); - } catch (InterruptedException e) { - } - } - listener.done(); - verify(out).println("Starting test"); - verify(out).println(String.format("%.2f (10/10) - Completed test", 100f)); - verify(out, times(expectedLogCount)).println(Mockito.anyString()); - } + private void testProgressLoggingListenerWithSpecifiedWaits(int unitsOfWork, long timeWait, long throttle, + int expectedLogCount) { + // When running maven-surefire System.out is replaced with a PrintStream that mockito cannot spy on, so we need to wrap it here + PrintStream wrapped = new PrintStream(System.out); + PrintStream out = spy(wrapped); + Listener listener = new ProgressLoggingListener("test", out).setTimeWait(timeWait); + listener.begin(unitsOfWork); + for (int step = 0; step < unitsOfWork; step++) { + listener.worked(1); + try { + Thread.sleep(throttle); + } catch (InterruptedException e) { + } + } + listener.done(); + verify(out).println("Starting test"); + verify(out).println(String.format("%.2f (10/10) - Completed test", 100f)); + verify(out, times(expectedLogCount)).println(Mockito.anyString()); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/RTreeBulkInsertTest.java b/src/test/java/org/neo4j/gis/spatial/RTreeBulkInsertTest.java index 319cd5d45..0611b44a4 100644 --- a/src/test/java/org/neo4j/gis/spatial/RTreeBulkInsertTest.java +++ b/src/test/java/org/neo4j/gis/spatial/RTreeBulkInsertTest.java @@ -1,1685 +1,1794 @@ package org.neo4j.gis.spatial; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; +import static org.neo4j.gis.spatial.rtree.RTreeIndex.DEFAULT_MAX_NODE_REFERENCES; +import static org.neo4j.internal.helpers.collection.MapUtil.map; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.commons.io.FileUtils; +import org.geotools.api.feature.simple.SimpleFeatureType; +import org.geotools.api.referencing.FactoryException; +import org.geotools.api.referencing.crs.CoordinateReferenceSystem; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.referencing.crs.DefaultEngineeringCRS; -import org.junit.*; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.dbms.api.DatabaseManagementServiceBuilder; import org.neo4j.gis.spatial.encoders.SimplePointEncoder; -import org.neo4j.gis.spatial.index.*; +import org.neo4j.gis.spatial.index.ExplicitIndexBackedMonitor; +import org.neo4j.gis.spatial.index.ExplicitIndexBackedPointIndex; +import org.neo4j.gis.spatial.index.IndexManager; +import org.neo4j.gis.spatial.index.LayerGeohashPointIndex; +import org.neo4j.gis.spatial.index.LayerHilbertPointIndex; +import org.neo4j.gis.spatial.index.LayerIndexReader; +import org.neo4j.gis.spatial.index.LayerZOrderPointIndex; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; import org.neo4j.gis.spatial.pipes.GeoPipeline; import org.neo4j.gis.spatial.rtree.Envelope; -import org.neo4j.gis.spatial.rtree.*; -import org.neo4j.graphdb.*; +import org.neo4j.gis.spatial.rtree.RTreeImageExporter; +import org.neo4j.gis.spatial.rtree.RTreeIndex; +import org.neo4j.gis.spatial.rtree.RTreeMonitor; +import org.neo4j.gis.spatial.rtree.RTreeRelationshipTypes; +import org.neo4j.gis.spatial.rtree.TreeMonitor; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.ResourceIterator; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import org.geotools.api.feature.simple.SimpleFeatureType; -import org.geotools.api.referencing.FactoryException; -import org.geotools.api.referencing.crs.CoordinateReferenceSystem; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.junit.Assert.*; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; -import static org.neo4j.gis.spatial.rtree.RTreeIndex.DEFAULT_MAX_NODE_REFERENCES; -import static org.neo4j.internal.helpers.collection.MapUtil.map; public class RTreeBulkInsertTest { - private DatabaseManagementService databases; - private GraphDatabaseService db; - private final File storeDir = new File("target/store").getAbsoluteFile(); - - // While the current lucene index implmentation is so slow (16n/s) we disable all benchmarks for lucene backed indexes - private static final boolean enableLucene = false; - - @Before - public void before() throws IOException { - restart(); - } - - @After - public void after() throws IOException { - doCleanShutdown(); - } - - @Ignore - public void shouldDeleteRecursiveTree() { - int depth = 5; - int width = 2; - //Create nodes - ArrayList> nodes = new ArrayList<>(); - try (Transaction tx = db.beginTx()) { - nodes.add(new ArrayList<>()); - nodes.get(0).add(tx.createNode()); - nodes.get(0).get(0).setProperty("name", "0-0"); - - for (int i = 1; i < depth; i++) { - ArrayList children = new ArrayList<>(); - nodes.add(children); - for (Node parent : nodes.get(i - 1)) { - for (int j = 0; j < width; j++) { - Node node = tx.createNode(); - node.setProperty("name", "" + i + "-" + j); - parent.createRelationshipTo(node, RTreeRelationshipTypes.RTREE_CHILD); - children.add(node); - } - } - } - debugRest(); - //Disconact leafs - ArrayList leaves = nodes.get(nodes.size() - 1); - for (Node leaf : leaves) { - leaf.getSingleRelationship(RTreeRelationshipTypes - .RTREE_CHILD, Direction.INCOMING).delete(); - - } - - deleteRecursivelySubtree(nodes.get(0).get(0), null); - System.out.println("Leaf"); - nodes.get(nodes.size() - 1).forEach(System.out::println); - tx.commit(); - } - debugRest(); - } - - private void debugRest() { - try (Transaction tx = db.beginTx()) { - Result result = tx.execute("MATCH (n) OPTIONAL MATCH (n)-[r]-() RETURN elementId(n), n.name, count(r)"); - while (result.hasNext()) { - System.out.println(result.next()); - } - tx.commit(); - } - } - - private void deleteRecursivelySubtree(Node node, Relationship incoming) { - for (Relationship relationship : node.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - deleteRecursivelySubtree(relationship.getEndNode(), relationship); - } - if (incoming != null) { - incoming.delete(); - } + private DatabaseManagementService databases; + private GraphDatabaseService db; + private final File storeDir = new File("target/store").getAbsoluteFile(); + + // While the current lucene index implmentation is so slow (16n/s) we disable all benchmarks for lucene backed indexes + private static final boolean enableLucene = false; + + @Before + public void before() throws IOException { + restart(); + } + + @After + public void after() throws IOException { + doCleanShutdown(); + } + + @Ignore + public void shouldDeleteRecursiveTree() { + int depth = 5; + int width = 2; + //Create nodes + ArrayList> nodes = new ArrayList<>(); + try (Transaction tx = db.beginTx()) { + nodes.add(new ArrayList<>()); + nodes.get(0).add(tx.createNode()); + nodes.get(0).get(0).setProperty("name", "0-0"); + + for (int i = 1; i < depth; i++) { + ArrayList children = new ArrayList<>(); + nodes.add(children); + for (Node parent : nodes.get(i - 1)) { + for (int j = 0; j < width; j++) { + Node node = tx.createNode(); + node.setProperty("name", "" + i + "-" + j); + parent.createRelationshipTo(node, RTreeRelationshipTypes.RTREE_CHILD); + children.add(node); + } + } + } + debugRest(); + //Disconact leafs + ArrayList leaves = nodes.get(nodes.size() - 1); + for (Node leaf : leaves) { + leaf.getSingleRelationship(RTreeRelationshipTypes + .RTREE_CHILD, Direction.INCOMING).delete(); + + } + + deleteRecursivelySubtree(nodes.get(0).get(0), null); + System.out.println("Leaf"); + nodes.get(nodes.size() - 1).forEach(System.out::println); + tx.commit(); + } + debugRest(); + } + + private void debugRest() { + try (Transaction tx = db.beginTx()) { + Result result = tx.execute("MATCH (n) OPTIONAL MATCH (n)-[r]-() RETURN elementId(n), n.name, count(r)"); + while (result.hasNext()) { + System.out.println(result.next()); + } + tx.commit(); + } + } + + private void deleteRecursivelySubtree(Node node, Relationship incoming) { + for (Relationship relationship : node.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + deleteRecursivelySubtree(relationship.getEndNode(), relationship); + } + if (incoming != null) { + incoming.delete(); + } // Iterator itr = node.getRelationships().iterator(); // while(itr.hasNext()){ // itr.next().delete(); // } - System.out.println(node.getElementId()); - node.delete(); - } - - SpatialDatabaseService spatial() { - return new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); - } - - private EditableLayer getOrCreateSimplePointLayer(String name, String index, String xProperty, String yProperty) { - CoordinateReferenceSystem crs = DefaultEngineeringCRS.GENERIC_2D; - try (Transaction tx = db.beginTx()) { - SpatialDatabaseService sdbs = spatial(); - EditableLayer layer = sdbs.getOrCreateSimplePointLayer(tx, name, index, xProperty, yProperty); - layer.setCoordinateReferenceSystem(tx, crs); - tx.commit(); - return layer; - } - } - - @Ignore - public void shouldInsertSimpleRTree() { - int width = 20; - int blockSize = 10000; - CoordinateReferenceSystem crs = DefaultEngineeringCRS.GENERIC_2D; - EditableLayer layer = getOrCreateSimplePointLayer("Coordinates", "rtree", "lon", "lat"); - List nodes = new ArrayList<>(); - try (Transaction tx = db.beginTx()) { - for (int i = 0; i < width; i++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", i); - node.setProperty("lon", 0); - nodes.add(node.getElementId()); - node.toString(); - } - tx.commit(); - } + System.out.println(node.getElementId()); + node.delete(); + } + + SpatialDatabaseService spatial() { + return new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); + } + + private EditableLayer getOrCreateSimplePointLayer(String name, String index, String xProperty, String yProperty) { + CoordinateReferenceSystem crs = DefaultEngineeringCRS.GENERIC_2D; + try (Transaction tx = db.beginTx()) { + SpatialDatabaseService sdbs = spatial(); + EditableLayer layer = sdbs.getOrCreateSimplePointLayer(tx, name, index, xProperty, yProperty); + layer.setCoordinateReferenceSystem(tx, crs); + tx.commit(); + return layer; + } + } + + @Ignore + public void shouldInsertSimpleRTree() { + int width = 20; + int blockSize = 10000; + CoordinateReferenceSystem crs = DefaultEngineeringCRS.GENERIC_2D; + EditableLayer layer = getOrCreateSimplePointLayer("Coordinates", "rtree", "lon", "lat"); + List nodes = new ArrayList<>(); + try (Transaction tx = db.beginTx()) { + for (int i = 0; i < width; i++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", i); + node.setProperty("lon", 0); + nodes.add(node.getElementId()); + node.toString(); + } + tx.commit(); + } // java.util.Collections.shuffle( nodes,new Random( 1 ) ); - TreeMonitor monitor = new RTreeMonitor(); - layer.getIndex().addMonitor(monitor); - long start = System.currentTimeMillis(); - - List list1 = nodes.subList(0, nodes.size() / 2 + 8); - List list2 = nodes.subList(list1.size(), nodes.size()); - System.out.println(list1); - System.out.println(list2); - try (Transaction tx = db.beginTx()) { - layer.addAll(tx, idsToNodes(tx, list1)); - tx.commit(); - } - Neo4jTestUtils.debugIndexTree(db, "Coordinates"); - //TODO add this part to the test + TreeMonitor monitor = new RTreeMonitor(); + layer.getIndex().addMonitor(monitor); + long start = System.currentTimeMillis(); + + List list1 = nodes.subList(0, nodes.size() / 2 + 8); + List list2 = nodes.subList(list1.size(), nodes.size()); + System.out.println(list1); + System.out.println(list2); + try (Transaction tx = db.beginTx()) { + layer.addAll(tx, idsToNodes(tx, list1)); + tx.commit(); + } + Neo4jTestUtils.debugIndexTree(db, "Coordinates"); + //TODO add this part to the test // try (Transaction tx = db.beginTx()) { // layer.addAll(list2); // tx.commit(); // } - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + (width * width) + " nodes to RTree in bulk"); + System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + (width * width) + + " nodes to RTree in bulk"); // queryRTree(layer); // verifyTreeStructure(layer); - Neo4jTestUtils.debugIndexTree(db, "Coordinates"); - - } - - private List idsToNodes(Transaction tx, List nodeIds) { - return nodeIds.stream().map(tx::getNodeByElementId).collect(Collectors.toList()); - } - - private static class IndexTestConfig { - String name; - int width; - Coordinate searchMin; - Coordinate searchMax; - long totalCount; - long expectedCount; - long expectedGeometries; - - /* - * Collection of test settings to perform assertions on. - * Note that due to some crazy GIS spec points on polygon edges are considered to be contained, - * unless the polygon is a rectangle, in which case they are not contained, leading to - * different numbers for expectedGeometries and expectedCount, - * See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 - */ - public IndexTestConfig(String name, int width, Coordinate searchMin, Coordinate searchMax, long expectedCount, long expectedGeometries) { - this.name = name; - this.width = width; - this.searchMin = searchMin; - this.searchMax = searchMax; - this.expectedCount = expectedCount; - this.expectedGeometries = expectedGeometries; - this.totalCount = width * width; - } - } - - private static final Map testConfigs = new HashMap<>(); - - static { - Coordinate searchMin = new Coordinate(0.5, 0.5); - Coordinate searchMax = new Coordinate(0.52, 0.52); - addTestConfig(new IndexTestConfig("very_small", 100, searchMin, searchMax, 9, 1)); - addTestConfig(new IndexTestConfig("small", 250, searchMin, searchMax, 35, 16)); - addTestConfig(new IndexTestConfig("medium", 500, searchMin, searchMax, 121, 81)); - addTestConfig(new IndexTestConfig("large", 750, searchMin, searchMax, 256, 196)); - } - - private static void addTestConfig(IndexTestConfig config) { - testConfigs.put(config.name, config); - } - - private interface IndexMaker { - EditableLayer setupLayer(Transaction tx); - - List nodes(); - - TestStats initStats(int blockSize); - - TimedLogger initLogger(); - - IndexTestConfig getConfig(); - - void verifyStructure(); - } - - private class GeohashIndexMaker implements IndexMaker { - private final String name; - private final String insertMode; - private final IndexTestConfig config; - private List nodes; - private EditableLayer layer; - - private GeohashIndexMaker(String name, String insertMode, IndexTestConfig config) { - this.name = name; - this.insertMode = insertMode; - this.config = config; - } - - @Override - public EditableLayer setupLayer(Transaction tx) { - this.nodes = setup(name, "geohash", config.width); - this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); - return layer; - } - - @Override - public List nodes() { - return nodes; - } - - @Override - public TestStats initStats(int blockSize) { - return new TestStats(config, insertMode, "Geohash", blockSize, -1); - } - - @Override - public TimedLogger initLogger() { - return new TimedLogger("Inserting " + config.totalCount + " nodes into Geohash using " + insertMode + " insert", config.totalCount); - } - - @Override - public IndexTestConfig getConfig() { - return config; - } - - @Override - public void verifyStructure() { - verifyGeohashIndex(layer); - } - } - - private class ZOrderIndexMaker implements IndexMaker { - private final SpatialDatabaseService spatial = spatial(); - private final String name; - private final String insertMode; - private final IndexTestConfig config; - private List nodes; - private EditableLayer layer; - - private ZOrderIndexMaker(String name, String insertMode, IndexTestConfig config) { - this.name = name; - this.insertMode = insertMode; - this.config = config; - } - - @Override - public EditableLayer setupLayer(Transaction tx) { - this.nodes = setup(name, "zorder", config.width); - this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); - return layer; - } - - @Override - public List nodes() { - return nodes; - } - - @Override - public TestStats initStats(int blockSize) { - return new TestStats(config, insertMode, "Z-Order", blockSize, -1); - } - - @Override - public TimedLogger initLogger() { - return new TimedLogger("Inserting " + config.totalCount + " nodes into Z-Order using " + insertMode + " insert", config.totalCount); - } - - @Override - public IndexTestConfig getConfig() { - return config; - } - - @Override - public void verifyStructure() { - verifyZOrderIndex(layer); - } - } - - private class HilbertIndexMaker implements IndexMaker { - private final String name; - private final String insertMode; - private final IndexTestConfig config; - private List nodes; - private EditableLayer layer; - - private HilbertIndexMaker(String name, String insertMode, IndexTestConfig config) { - this.name = name; - this.insertMode = insertMode; - this.config = config; - } - - @Override - public EditableLayer setupLayer(Transaction tx) { - this.nodes = setup(name, "hilbert", config.width); - this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); - return layer; - } - - @Override - public List nodes() { - return nodes; - } - - @Override - public TestStats initStats(int blockSize) { - return new TestStats(config, insertMode, "Hilbert", blockSize, -1); - } - - @Override - public TimedLogger initLogger() { - return new TimedLogger("Inserting " + config.totalCount + " nodes into Hilbert using " + insertMode + " insert", config.totalCount); - } - - @Override - public IndexTestConfig getConfig() { - return config; - } - - @Override - public void verifyStructure() { - verifyHilbertIndex(layer); - } - } - - private class RTreeIndexMaker implements IndexMaker { - private final SpatialDatabaseService spatial = spatial(); - private final String splitMode; - private final String insertMode; - private final boolean shouldMergeTrees; - private final int maxNodeReferences; - private final IndexTestConfig config; - private final String name; - private EditableLayer layer; - private TestStats stats; - private List nodes; - - private RTreeIndexMaker(String name, String splitMode, String insertMode, int maxNodeReferences, IndexTestConfig config) { - this(name, splitMode, insertMode, maxNodeReferences, config, false); - } - - private RTreeIndexMaker(String name, String splitMode, String insertMode, int maxNodeReferences, IndexTestConfig config, boolean shouldMergeTrees) { - this.name = name; - this.splitMode = splitMode; - this.insertMode = insertMode; - this.shouldMergeTrees = shouldMergeTrees; - this.maxNodeReferences = maxNodeReferences; - this.config = config; - } - - public EditableLayer setupLayer(Transaction tx) { - this.nodes = setup(name, "rtree", config.width); - this.layer = (EditableLayer) spatial.getLayer(tx, name); - layer.getIndex().configure(map( - RTreeIndex.KEY_SPLIT, splitMode, - RTreeIndex.KEY_MAX_NODE_REFERENCES, maxNodeReferences, - RTreeIndex.KEY_SHOULD_MERGE_TREES, shouldMergeTrees) - ); - return layer; - } - - @Override - public List nodes() { - return nodes; - } - - @Override - public TestStats initStats(int blockSize) { - this.stats = new TestStats(config, insertMode, splitMode, blockSize, maxNodeReferences); - return this.stats; - } - - @Override - public TimedLogger initLogger() { - return new TimedLogger("Inserting " + config.totalCount + " nodes into RTree using " + insertMode + " insert and " - + splitMode + " split with " + maxNodeReferences + " maxNodeReferences", config.totalCount); - } - - @Override - public IndexTestConfig getConfig() { - return config; - } - - @Override - public void verifyStructure() { - verifyTreeStructure(layer, splitMode, stats); - } - - } - - /* - * Very small model 100*100 nodes - */ - - @Test - public void shouldInsertManyNodesIndividuallyWithGeohash_very_small() throws FactoryException, IOException { - insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithGeohash_very_small() throws FactoryException, IOException { - insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithZOrder_very_small() throws FactoryException, IOException { - insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithZOrder_very_small() throws FactoryException, IOException { - insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithHilbert_very_small() throws FactoryException, IOException { - insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithHilbert_very_small() throws FactoryException, IOException { - insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_very_small_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("very_small")); - } - - @Test - public void shouldInsertManyNodesIndividuallyGreenesSplit_very_small_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("very_small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithQuadraticSplit_very_small_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("very_small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithGreenesSplit_very_small_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("very_small")); - } - - /* - * Small model 250*250 nodes - */ - - @Test - public void shouldInsertManyNodesIndividuallyWithGeohash_small() throws FactoryException, IOException { - insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithGeohash_small() throws FactoryException, IOException { - insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithZOrder_small() throws FactoryException, IOException { - insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithZOrder_small() throws FactoryException, IOException { - insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithHilbert_small() throws FactoryException, IOException { - insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithHilbert_small() throws FactoryException, IOException { - insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_small_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("small")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesIndividuallyGreenesSplit_small_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithQuadraticSplit_small_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithGreenesSplit_small_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("small")); - } - - /* - * Small model 250*250 nodes (shallow tree) - */ - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_small_100() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("small")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesIndividuallyGreenesSplit_small_100() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithQuadraticSplit_small_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("small")); - } - - @Test - public void shouldInsertManyNodesInBulkWithGreenesSplit_small_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("small")); - } - - /* - * Medium model 500*500 nodes (deep tree - factor 10) - */ - - @Test - public void shouldInsertManyNodesIndividuallyWithGeohash_medium() throws FactoryException, IOException { - insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithGeohash_medium() throws FactoryException, IOException { - insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithZOrder_medium() throws FactoryException, IOException { - insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithZOrder_medium() throws FactoryException, IOException { - insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithHilbert_medium() throws FactoryException, IOException { - insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithHilbert_medium() throws FactoryException, IOException { - insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); - } - - @Ignore - public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_medium_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium")); - } - - @Ignore - public void shouldInsertManyNodesIndividuallyGreenesSplit_medium_10() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium")); - } - - @Test - public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium")); - } - - @Test - public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium")); - } - - @Ignore - public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_10_merge() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium"), true); - } - - @Ignore - public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_10_merge() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium"), true); - } - - /* - * Medium model 500*500 nodes (shallow tree - factor 100) - */ - - @Ignore - public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_medium_100() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium")); - } - - @Ignore - public void shouldInsertManyNodesIndividuallyGreenesSplit_medium_100() throws FactoryException, IOException { - insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium")); - } - - @Test - public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium")); - } - - @Ignore - public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_100_merge() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium"), true); - } - - @Ignore - public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_100_merge() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium"), true); - } - - /* - * Large model 750*750 nodes (only test bulk insert, 100 and 10, green and quadratic) - */ - - @Test - public void shouldInsertManyNodesIndividuallyWithGeohash_large() throws FactoryException, IOException { - insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithGeohash_large() throws FactoryException, IOException { - insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithZOrder_large() throws FactoryException, IOException { - insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithZOrder_large() throws FactoryException, IOException { - insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); - } - - @Test - public void shouldInsertManyNodesIndividuallyWithHilbert_large() throws FactoryException, IOException { - insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); - } - - @Test - public void shouldInsertManyNodesInBulkWithHilbert_large() throws FactoryException, IOException { - insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesInBulkWithQuadraticSplit_large_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("large")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesInBulkWithGreenesSplit_large_10() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("large")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesInBulkWithQuadraticSplit_large_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("large")); - } - - @Ignore // takes too long, change to @Test when benchmarking - public void shouldInsertManyNodesInBulkWithGreenesSplit_large_100() throws FactoryException, IOException { - insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("large")); - } - - /* - * Private methods used by the above tests - */ - - class TreePrintingMonitor extends RTreeMonitor { - private final RTreeImageExporter imageExporter; - private final String splitMode; - private final String insertMode; - private HashMap called = new HashMap<>(); - - TreePrintingMonitor(RTreeImageExporter imageExporter, String insertMode, String splitMode) { - this.imageExporter = imageExporter; - this.splitMode = splitMode; - this.insertMode = insertMode; - } - - private Integer getCalled(String key) { - if (!called.containsKey(key)) { - called.put(key, 0); - } - return called.get(key); - } - - @Override - public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) { - super.addNbrRebuilt(rtree, tx); - printRTreeImage("rebuilt", rtree.getIndexRoot(tx), new ArrayList<>()); - } - - @Override - public void addSplit(Node indexNode) { - super.addSplit(indexNode); + Neo4jTestUtils.debugIndexTree(db, "Coordinates"); + + } + + private List idsToNodes(Transaction tx, List nodeIds) { + return nodeIds.stream().map(tx::getNodeByElementId).collect(Collectors.toList()); + } + + private static class IndexTestConfig { + + String name; + int width; + Coordinate searchMin; + Coordinate searchMax; + long totalCount; + long expectedCount; + long expectedGeometries; + + /* + * Collection of test settings to perform assertions on. + * Note that due to some crazy GIS spec points on polygon edges are considered to be contained, + * unless the polygon is a rectangle, in which case they are not contained, leading to + * different numbers for expectedGeometries and expectedCount, + * See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 + */ + public IndexTestConfig(String name, int width, Coordinate searchMin, Coordinate searchMax, long expectedCount, + long expectedGeometries) { + this.name = name; + this.width = width; + this.searchMin = searchMin; + this.searchMax = searchMax; + this.expectedCount = expectedCount; + this.expectedGeometries = expectedGeometries; + this.totalCount = width * width; + } + } + + private static final Map testConfigs = new HashMap<>(); + + static { + Coordinate searchMin = new Coordinate(0.5, 0.5); + Coordinate searchMax = new Coordinate(0.52, 0.52); + addTestConfig(new IndexTestConfig("very_small", 100, searchMin, searchMax, 9, 1)); + addTestConfig(new IndexTestConfig("small", 250, searchMin, searchMax, 35, 16)); + addTestConfig(new IndexTestConfig("medium", 500, searchMin, searchMax, 121, 81)); + addTestConfig(new IndexTestConfig("large", 750, searchMin, searchMax, 256, 196)); + } + + private static void addTestConfig(IndexTestConfig config) { + testConfigs.put(config.name, config); + } + + private interface IndexMaker { + + EditableLayer setupLayer(Transaction tx); + + List nodes(); + + TestStats initStats(int blockSize); + + TimedLogger initLogger(); + + IndexTestConfig getConfig(); + + void verifyStructure(); + } + + private class GeohashIndexMaker implements IndexMaker { + + private final String name; + private final String insertMode; + private final IndexTestConfig config; + private List nodes; + private EditableLayer layer; + + private GeohashIndexMaker(String name, String insertMode, IndexTestConfig config) { + this.name = name; + this.insertMode = insertMode; + this.config = config; + } + + @Override + public EditableLayer setupLayer(Transaction tx) { + this.nodes = setup(name, "geohash", config.width); + this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); + return layer; + } + + @Override + public List nodes() { + return nodes; + } + + @Override + public TestStats initStats(int blockSize) { + return new TestStats(config, insertMode, "Geohash", blockSize, -1); + } + + @Override + public TimedLogger initLogger() { + return new TimedLogger( + "Inserting " + config.totalCount + " nodes into Geohash using " + insertMode + " insert", + config.totalCount); + } + + @Override + public IndexTestConfig getConfig() { + return config; + } + + @Override + public void verifyStructure() { + verifyGeohashIndex(layer); + } + } + + private class ZOrderIndexMaker implements IndexMaker { + + private final SpatialDatabaseService spatial = spatial(); + private final String name; + private final String insertMode; + private final IndexTestConfig config; + private List nodes; + private EditableLayer layer; + + private ZOrderIndexMaker(String name, String insertMode, IndexTestConfig config) { + this.name = name; + this.insertMode = insertMode; + this.config = config; + } + + @Override + public EditableLayer setupLayer(Transaction tx) { + this.nodes = setup(name, "zorder", config.width); + this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); + return layer; + } + + @Override + public List nodes() { + return nodes; + } + + @Override + public TestStats initStats(int blockSize) { + return new TestStats(config, insertMode, "Z-Order", blockSize, -1); + } + + @Override + public TimedLogger initLogger() { + return new TimedLogger( + "Inserting " + config.totalCount + " nodes into Z-Order using " + insertMode + " insert", + config.totalCount); + } + + @Override + public IndexTestConfig getConfig() { + return config; + } + + @Override + public void verifyStructure() { + verifyZOrderIndex(layer); + } + } + + private class HilbertIndexMaker implements IndexMaker { + + private final String name; + private final String insertMode; + private final IndexTestConfig config; + private List nodes; + private EditableLayer layer; + + private HilbertIndexMaker(String name, String insertMode, IndexTestConfig config) { + this.name = name; + this.insertMode = insertMode; + this.config = config; + } + + @Override + public EditableLayer setupLayer(Transaction tx) { + this.nodes = setup(name, "hilbert", config.width); + this.layer = (EditableLayer) spatial().getLayer(tx, "Coordinates"); + return layer; + } + + @Override + public List nodes() { + return nodes; + } + + @Override + public TestStats initStats(int blockSize) { + return new TestStats(config, insertMode, "Hilbert", blockSize, -1); + } + + @Override + public TimedLogger initLogger() { + return new TimedLogger( + "Inserting " + config.totalCount + " nodes into Hilbert using " + insertMode + " insert", + config.totalCount); + } + + @Override + public IndexTestConfig getConfig() { + return config; + } + + @Override + public void verifyStructure() { + verifyHilbertIndex(layer); + } + } + + private class RTreeIndexMaker implements IndexMaker { + + private final SpatialDatabaseService spatial = spatial(); + private final String splitMode; + private final String insertMode; + private final boolean shouldMergeTrees; + private final int maxNodeReferences; + private final IndexTestConfig config; + private final String name; + private EditableLayer layer; + private TestStats stats; + private List nodes; + + private RTreeIndexMaker(String name, String splitMode, String insertMode, int maxNodeReferences, + IndexTestConfig config) { + this(name, splitMode, insertMode, maxNodeReferences, config, false); + } + + private RTreeIndexMaker(String name, String splitMode, String insertMode, int maxNodeReferences, + IndexTestConfig config, boolean shouldMergeTrees) { + this.name = name; + this.splitMode = splitMode; + this.insertMode = insertMode; + this.shouldMergeTrees = shouldMergeTrees; + this.maxNodeReferences = maxNodeReferences; + this.config = config; + } + + public EditableLayer setupLayer(Transaction tx) { + this.nodes = setup(name, "rtree", config.width); + this.layer = (EditableLayer) spatial.getLayer(tx, name); + layer.getIndex().configure(map( + RTreeIndex.KEY_SPLIT, splitMode, + RTreeIndex.KEY_MAX_NODE_REFERENCES, maxNodeReferences, + RTreeIndex.KEY_SHOULD_MERGE_TREES, shouldMergeTrees) + ); + return layer; + } + + @Override + public List nodes() { + return nodes; + } + + @Override + public TestStats initStats(int blockSize) { + this.stats = new TestStats(config, insertMode, splitMode, blockSize, maxNodeReferences); + return this.stats; + } + + @Override + public TimedLogger initLogger() { + return new TimedLogger( + "Inserting " + config.totalCount + " nodes into RTree using " + insertMode + " insert and " + + splitMode + " split with " + maxNodeReferences + " maxNodeReferences", config.totalCount); + } + + @Override + public IndexTestConfig getConfig() { + return config; + } + + @Override + public void verifyStructure() { + verifyTreeStructure(layer, splitMode, stats); + } + + } + + /* + * Very small model 100*100 nodes + */ + + @Test + public void shouldInsertManyNodesIndividuallyWithGeohash_very_small() throws FactoryException, IOException { + insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), + 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithGeohash_very_small() throws FactoryException, IOException { + insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithZOrder_very_small() throws FactoryException, IOException { + insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithZOrder_very_small() throws FactoryException, IOException { + insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithHilbert_very_small() throws FactoryException, IOException { + insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("very_small")), + 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithHilbert_very_small() throws FactoryException, IOException { + insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("very_small")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_very_small_10() + throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("very_small")); + } + + @Test + public void shouldInsertManyNodesIndividuallyGreenesSplit_very_small_10() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("very_small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithQuadraticSplit_very_small_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("very_small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithGreenesSplit_very_small_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("very_small")); + } + + /* + * Small model 250*250 nodes + */ + + @Test + public void shouldInsertManyNodesIndividuallyWithGeohash_small() throws FactoryException, IOException { + insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithGeohash_small() throws FactoryException, IOException { + insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithZOrder_small() throws FactoryException, IOException { + insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithZOrder_small() throws FactoryException, IOException { + insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithHilbert_small() throws FactoryException, IOException { + insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("small")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithHilbert_small() throws FactoryException, IOException { + insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("small")), 5000); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_small_10() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("small")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesIndividuallyGreenesSplit_small_10() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithQuadraticSplit_small_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithGreenesSplit_small_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("small")); + } + + /* + * Small model 250*250 nodes (shallow tree) + */ + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_small_100() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("small")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesIndividuallyGreenesSplit_small_100() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithQuadraticSplit_small_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("small")); + } + + @Test + public void shouldInsertManyNodesInBulkWithGreenesSplit_small_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("small")); + } + + /* + * Medium model 500*500 nodes (deep tree - factor 10) + */ + + @Test + public void shouldInsertManyNodesIndividuallyWithGeohash_medium() throws FactoryException, IOException { + insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithGeohash_medium() throws FactoryException, IOException { + insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithZOrder_medium() throws FactoryException, IOException { + insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithZOrder_medium() throws FactoryException, IOException { + insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithHilbert_medium() throws FactoryException, IOException { + insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("medium")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithHilbert_medium() throws FactoryException, IOException { + insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("medium")), 5000); + } + + @Ignore + public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_medium_10() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium")); + } + + @Ignore + public void shouldInsertManyNodesIndividuallyGreenesSplit_medium_10() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium")); + } + + @Test + public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium")); + } + + @Test + public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium")); + } + + @Ignore + public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_10_merge() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("medium"), true); + } + + @Ignore + public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_10_merge() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("medium"), true); + } + + /* + * Medium model 500*500 nodes (shallow tree - factor 100) + */ + + @Ignore + public void shouldInsertManyNodesIndividuallyWithQuadraticSplit_medium_100() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium")); + } + + @Ignore + public void shouldInsertManyNodesIndividuallyGreenesSplit_medium_100() throws FactoryException, IOException { + insertManyNodesIndividually(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium")); + } + + @Test + public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium")); + } + + @Ignore + public void shouldInsertManyNodesInBulkWithQuadraticSplit_medium_100_merge() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("medium"), true); + } + + @Ignore + public void shouldInsertManyNodesInBulkWithGreenesSplit_medium_100_merge() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("medium"), true); + } + + /* + * Large model 750*750 nodes (only test bulk insert, 100 and 10, green and quadratic) + */ + + @Test + public void shouldInsertManyNodesIndividuallyWithGeohash_large() throws FactoryException, IOException { + insertManyNodesIndividually(new GeohashIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithGeohash_large() throws FactoryException, IOException { + insertManyNodesInBulk(new GeohashIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithZOrder_large() throws FactoryException, IOException { + insertManyNodesIndividually(new ZOrderIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithZOrder_large() throws FactoryException, IOException { + insertManyNodesInBulk(new ZOrderIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); + } + + @Test + public void shouldInsertManyNodesIndividuallyWithHilbert_large() throws FactoryException, IOException { + insertManyNodesIndividually(new HilbertIndexMaker("Coordinates", "Single", testConfigs.get("large")), 5000); + } + + @Test + public void shouldInsertManyNodesInBulkWithHilbert_large() throws FactoryException, IOException { + insertManyNodesInBulk(new HilbertIndexMaker("Coordinates", "Bulk", testConfigs.get("large")), 5000); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesInBulkWithQuadraticSplit_large_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 10, testConfigs.get("large")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesInBulkWithGreenesSplit_large_10() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 10, testConfigs.get("large")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesInBulkWithQuadraticSplit_large_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.QUADRATIC_SPLIT, 5000, 100, testConfigs.get("large")); + } + + @Ignore // takes too long, change to @Test when benchmarking + public void shouldInsertManyNodesInBulkWithGreenesSplit_large_100() throws FactoryException, IOException { + insertManyNodesInBulk(RTreeIndex.GREENES_SPLIT, 5000, 100, testConfigs.get("large")); + } + + /* + * Private methods used by the above tests + */ + + class TreePrintingMonitor extends RTreeMonitor { + + private final RTreeImageExporter imageExporter; + private final String splitMode; + private final String insertMode; + private HashMap called = new HashMap<>(); + + TreePrintingMonitor(RTreeImageExporter imageExporter, String insertMode, String splitMode) { + this.imageExporter = imageExporter; + this.splitMode = splitMode; + this.insertMode = insertMode; + } + + private Integer getCalled(String key) { + if (!called.containsKey(key)) { + called.put(key, 0); + } + return called.get(key); + } + + @Override + public void addNbrRebuilt(RTreeIndex rtree, Transaction tx) { + super.addNbrRebuilt(rtree, tx); + printRTreeImage("rebuilt", rtree.getIndexRoot(tx), new ArrayList<>()); + } + + @Override + public void addSplit(Node indexNode) { + super.addSplit(indexNode); // printRTreeImage("split", indexNode, new ArrayList<>()); - } - - @Override - public void beforeMergeTree(Node indexNode, List right) { - super.beforeMergeTree(indexNode, right); - - printRTreeImage("before-merge", indexNode, right.stream().map(e -> e.envelope).collect(Collectors.toList())); - - } - - @Override - public void afterMergeTree(Node indexNode) { - super.afterMergeTree(indexNode); - printRTreeImage("after-merge", indexNode, new ArrayList<>()); - } - - - private void printRTreeImage(String context, Node rootNode, List envelopes) { - try (Transaction tx = db.beginTx()) { - int count = getCalled(context); - imageExporter.saveRTreeLayers(tx, - new File("rtree-" + insertMode + "-" + splitMode + "/debug-" + context + "/rtree-" + count + ".png"), - rootNode, envelopes, 7); - called.put(context, count + 1); - tx.commit(); - } catch (IOException e) { - System.out.println("Failed to print RTree to disk: " + e.getMessage()); - e.printStackTrace(); - } - } - } - - private void insertManyNodesIndividually(String splitMode, int blockSize, int maxNodeReferences, IndexTestConfig config) - throws FactoryException, IOException { - insertManyNodesIndividually(new RTreeIndexMaker("Coordinates", splitMode, "Single", maxNodeReferences, config), blockSize); - } - - private EditableLayer setupLayer(IndexMaker indexMaker) { - try (Transaction tx = db.beginTx()) { - EditableLayer layer = indexMaker.setupLayer(tx); - tx.commit(); - return layer; - } - } - - private void insertManyNodesIndividually(IndexMaker indexMaker, int blockSize) { - if (enableLucene || indexMaker instanceof RTreeIndexMaker) { - TestStats stats = indexMaker.initStats(blockSize); - EditableLayer layer = setupLayer(indexMaker); - List nodes = indexMaker.nodes(); - TreeMonitor monitor = new RTreeMonitor(); - layer.getIndex().addMonitor(monitor); - TimedLogger log = indexMaker.initLogger(); - IndexTestConfig config = indexMaker.getConfig(); - long start = System.currentTimeMillis(); - for (int i = 0; i < config.totalCount / blockSize; i++) { - List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); - try (Transaction tx = db.beginTx()) { - for (String node : slice) { - layer.add(tx, tx.getNodeByElementId(node)); - } - tx.commit(); - } - log.log("Splits: " + monitor.getNbrSplit(), (i + 1) * blockSize); - } - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + " nodes to RTree in bulk"); - stats.setInsertTime(start); - stats.put("Insert Splits", monitor.getNbrSplit()); - - queryRTree(layer, monitor, stats); - indexMaker.verifyStructure(); - } - } - - /* - * Run this manually to generate images of RTree that can be used for animation. - * ffmpeg -f image2 -r 12 -i rtree-single/rtree-%d.png -r 12 -s 1280x960 rtree-single2_12fps.mp4 - */ - @Ignore - public void shouldInsertManyNodesIndividuallyAndGenerateImagesForAnimation() throws FactoryException, IOException { - IndexTestConfig config = testConfigs.get("medium"); - int blockSize = 5; - int maxBlockSize = 1000; - int maxNodeReferences = 10; - String splitMode = RTreeIndex.GREENES_SPLIT; - IndexMaker indexMaker = new RTreeIndexMaker("Coordinates", splitMode, "Single", maxNodeReferences, config); - TestStats stats = indexMaker.initStats(blockSize); - EditableLayer layer = setupLayer(indexMaker); - List nodes = indexMaker.nodes(); - - RTreeIndex rtree = (RTreeIndex) layer.getIndex(); - RTreeImageExporter imageExporter; - try (Transaction tx = db.beginTx()) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); - imageExporter = new RTreeImageExporter(layer.getGeometryFactory(), layer.getGeometryEncoder(), layer.getCoordinateReferenceSystem(tx), featureType, rtree); - imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); - tx.commit(); - } - - TreeMonitor monitor = new TreePrintingMonitor(imageExporter, "single", splitMode); - layer.getIndex().addMonitor(monitor); - TimedLogger log = indexMaker.initLogger(); - long start = System.currentTimeMillis(); - int prevBlock = 0; - int i = 0; - int currBlock = 1; - while (currBlock < nodes.size()) { - List slice = nodes.subList(prevBlock, currBlock); - long startIndexing = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - for (String node : slice) { - layer.add(tx, tx.getNodeByElementId(node)); - } - tx.commit(); - } - log.log("Splits: " + monitor.getNbrSplit(), currBlock); - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("rtree-single-" + splitMode + "/rtree-" + i + ".png"), 7); - tx.commit(); - } - i++; - prevBlock = currBlock; - currBlock += Math.min(blockSize, maxBlockSize); - blockSize *= 1.33; - } - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + " nodes to RTree in bulk"); - stats.setInsertTime(start); - stats.put("Insert Splits", monitor.getNbrSplit()); - - monitor.reset(); - List found = queryRTree(layer, monitor, stats, false); - verifyTreeStructure(layer, splitMode, stats); - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("rtree-single-" + splitMode + "/rtree.png"), 7, monitor, found, config.searchMin, config.searchMax); - tx.commit(); - } - } - - private void insertManyNodesInBulk(String splitMode, int blockSize, int maxNodeReferences, IndexTestConfig config) - throws FactoryException, IOException { - insertManyNodesInBulk(new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config, false), blockSize); - } - - private void insertManyNodesInBulk(String splitMode, int blockSize, int maxNodeReferences, IndexTestConfig config, - boolean shouldMergeTrees) throws FactoryException, IOException { - insertManyNodesInBulk(new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config, shouldMergeTrees), blockSize); - } - - private void insertManyNodesInBulk(IndexMaker indexMaker, int blockSize) { - if (enableLucene || indexMaker instanceof RTreeIndexMaker) { - TestStats stats = indexMaker.initStats(blockSize); - EditableLayer layer = setupLayer(indexMaker); - List nodes = indexMaker.nodes(); - RTreeMonitor monitor = new RTreeMonitor(); - layer.getIndex().addMonitor(monitor); - TimedLogger log = indexMaker.initLogger(); - long start = System.currentTimeMillis(); - for (int i = 0; i < indexMaker.getConfig().totalCount / blockSize; i++) { - List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); - long startIndexing = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - layer.addAll(tx, idsToNodes(tx, slice)); - tx.commit(); - } - log.log(startIndexing, "Rebuilt: " + monitor.getNbrRebuilt() + ", Splits: " + monitor.getNbrSplit() + ", Cases " + monitor.getCaseCounts(), (i + 1) * blockSize); - } - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + indexMaker.getConfig().totalCount + " nodes to RTree in bulk"); - stats.setInsertTime(start); - stats.put("Insert Splits", monitor.getNbrSplit()); - - monitor.reset(); - queryRTree(layer, monitor, stats); - indexMaker.verifyStructure(); + } + + @Override + public void beforeMergeTree(Node indexNode, List right) { + super.beforeMergeTree(indexNode, right); + + printRTreeImage("before-merge", indexNode, + right.stream().map(e -> e.envelope).collect(Collectors.toList())); + + } + + @Override + public void afterMergeTree(Node indexNode) { + super.afterMergeTree(indexNode); + printRTreeImage("after-merge", indexNode, new ArrayList<>()); + } + + + private void printRTreeImage(String context, Node rootNode, List envelopes) { + try (Transaction tx = db.beginTx()) { + int count = getCalled(context); + imageExporter.saveRTreeLayers(tx, + new File("rtree-" + insertMode + "-" + splitMode + "/debug-" + context + "/rtree-" + count + + ".png"), + rootNode, envelopes, 7); + called.put(context, count + 1); + tx.commit(); + } catch (IOException e) { + System.out.println("Failed to print RTree to disk: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + private void insertManyNodesIndividually(String splitMode, int blockSize, int maxNodeReferences, + IndexTestConfig config) + throws FactoryException, IOException { + insertManyNodesIndividually(new RTreeIndexMaker("Coordinates", splitMode, "Single", maxNodeReferences, config), + blockSize); + } + + private EditableLayer setupLayer(IndexMaker indexMaker) { + try (Transaction tx = db.beginTx()) { + EditableLayer layer = indexMaker.setupLayer(tx); + tx.commit(); + return layer; + } + } + + private void insertManyNodesIndividually(IndexMaker indexMaker, int blockSize) { + if (enableLucene || indexMaker instanceof RTreeIndexMaker) { + TestStats stats = indexMaker.initStats(blockSize); + EditableLayer layer = setupLayer(indexMaker); + List nodes = indexMaker.nodes(); + TreeMonitor monitor = new RTreeMonitor(); + layer.getIndex().addMonitor(monitor); + TimedLogger log = indexMaker.initLogger(); + IndexTestConfig config = indexMaker.getConfig(); + long start = System.currentTimeMillis(); + for (int i = 0; i < config.totalCount / blockSize; i++) { + List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); + try (Transaction tx = db.beginTx()) { + for (String node : slice) { + layer.add(tx, tx.getNodeByElementId(node)); + } + tx.commit(); + } + log.log("Splits: " + monitor.getNbrSplit(), (i + 1) * blockSize); + } + System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + + " nodes to RTree in bulk"); + stats.setInsertTime(start); + stats.put("Insert Splits", monitor.getNbrSplit()); + + queryRTree(layer, monitor, stats); + indexMaker.verifyStructure(); + } + } + + /* + * Run this manually to generate images of RTree that can be used for animation. + * ffmpeg -f image2 -r 12 -i rtree-single/rtree-%d.png -r 12 -s 1280x960 rtree-single2_12fps.mp4 + */ + @Ignore + public void shouldInsertManyNodesIndividuallyAndGenerateImagesForAnimation() throws FactoryException, IOException { + IndexTestConfig config = testConfigs.get("medium"); + int blockSize = 5; + int maxBlockSize = 1000; + int maxNodeReferences = 10; + String splitMode = RTreeIndex.GREENES_SPLIT; + IndexMaker indexMaker = new RTreeIndexMaker("Coordinates", splitMode, "Single", maxNodeReferences, config); + TestStats stats = indexMaker.initStats(blockSize); + EditableLayer layer = setupLayer(indexMaker); + List nodes = indexMaker.nodes(); + + RTreeIndex rtree = (RTreeIndex) layer.getIndex(); + RTreeImageExporter imageExporter; + try (Transaction tx = db.beginTx()) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); + imageExporter = new RTreeImageExporter(layer.getGeometryFactory(), layer.getGeometryEncoder(), + layer.getCoordinateReferenceSystem(tx), featureType, rtree); + imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); + tx.commit(); + } + + TreeMonitor monitor = new TreePrintingMonitor(imageExporter, "single", splitMode); + layer.getIndex().addMonitor(monitor); + TimedLogger log = indexMaker.initLogger(); + long start = System.currentTimeMillis(); + int prevBlock = 0; + int i = 0; + int currBlock = 1; + while (currBlock < nodes.size()) { + List slice = nodes.subList(prevBlock, currBlock); + long startIndexing = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + for (String node : slice) { + layer.add(tx, tx.getNodeByElementId(node)); + } + tx.commit(); + } + log.log("Splits: " + monitor.getNbrSplit(), currBlock); + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("rtree-single-" + splitMode + "/rtree-" + i + ".png"), 7); + tx.commit(); + } + i++; + prevBlock = currBlock; + currBlock += Math.min(blockSize, maxBlockSize); + blockSize *= 1.33; + } + System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + + " nodes to RTree in bulk"); + stats.setInsertTime(start); + stats.put("Insert Splits", monitor.getNbrSplit()); + + monitor.reset(); + List found = queryRTree(layer, monitor, stats, false); + verifyTreeStructure(layer, splitMode, stats); + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("rtree-single-" + splitMode + "/rtree.png"), 7, monitor, found, + config.searchMin, config.searchMax); + tx.commit(); + } + } + + private void insertManyNodesInBulk(String splitMode, int blockSize, int maxNodeReferences, IndexTestConfig config) + throws FactoryException, IOException { + insertManyNodesInBulk(new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config, false), + blockSize); + } + + private void insertManyNodesInBulk(String splitMode, int blockSize, int maxNodeReferences, IndexTestConfig config, + boolean shouldMergeTrees) throws FactoryException, IOException { + insertManyNodesInBulk( + new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config, shouldMergeTrees), + blockSize); + } + + private void insertManyNodesInBulk(IndexMaker indexMaker, int blockSize) { + if (enableLucene || indexMaker instanceof RTreeIndexMaker) { + TestStats stats = indexMaker.initStats(blockSize); + EditableLayer layer = setupLayer(indexMaker); + List nodes = indexMaker.nodes(); + RTreeMonitor monitor = new RTreeMonitor(); + layer.getIndex().addMonitor(monitor); + TimedLogger log = indexMaker.initLogger(); + long start = System.currentTimeMillis(); + for (int i = 0; i < indexMaker.getConfig().totalCount / blockSize; i++) { + List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); + long startIndexing = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + layer.addAll(tx, idsToNodes(tx, slice)); + tx.commit(); + } + log.log(startIndexing, + "Rebuilt: " + monitor.getNbrRebuilt() + ", Splits: " + monitor.getNbrSplit() + ", Cases " + + monitor.getCaseCounts(), (i + 1) * blockSize); + } + System.out.println( + "Took " + (System.currentTimeMillis() - start) + "ms to add " + indexMaker.getConfig().totalCount + + " nodes to RTree in bulk"); + stats.setInsertTime(start); + stats.put("Insert Splits", monitor.getNbrSplit()); + + monitor.reset(); + queryRTree(layer, monitor, stats); + indexMaker.verifyStructure(); // debugIndexTree((RTreeIndex) layer.getIndex()); - } - } - - /* - * Run this manually to generate images of RTree that can be used for animation. - * ffmpeg -f image2 -r 12 -i rtree-single/rtree-%d.png -r 12 -s 1280x960 rtree-single2_12fps.mp4 - */ - @Ignore - public void shouldInsertManyNodesInBulkAndGenerateImagesForAnimation() throws FactoryException, IOException { - IndexTestConfig config = testConfigs.get("medium"); - int blockSize = 1000; - int maxNodeReferences = 10; - String splitMode = RTreeIndex.GREENES_SPLIT; - IndexMaker indexMaker = new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config); - EditableLayer layer = setupLayer(indexMaker); - List nodes = indexMaker.nodes(); - TestStats stats = indexMaker.initStats(blockSize); - - RTreeIndex rtree = (RTreeIndex) layer.getIndex(); - RTreeImageExporter imageExporter; - try (Transaction tx = db.beginTx()) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); - imageExporter = new RTreeImageExporter(layer.getGeometryFactory(), layer.getGeometryEncoder(), layer.getCoordinateReferenceSystem(tx), featureType, rtree); - imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); - tx.commit(); - } - - TreeMonitor monitor = new TreePrintingMonitor(imageExporter, "bulk", splitMode); - layer.getIndex().addMonitor(monitor); - TimedLogger log = indexMaker.initLogger(); - long start = System.currentTimeMillis(); - for (int i = 0; i < config.totalCount / blockSize; i++) { - List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); - long startIndexing = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - layer.addAll(tx, idsToNodes(tx, slice)); - tx.commit(); - } - log.log(startIndexing, "Rebuilt: " + monitor.getNbrRebuilt() + ", Splits: " + monitor.getNbrSplit() + ", Cases " + monitor.getCaseCounts(), (i + 1) * blockSize); - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("rtree-bulk-" + splitMode + "/rtree-" + i + ".png"), 7); - tx.commit(); - } - } - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + " nodes to RTree in bulk"); - stats.setInsertTime(start); - stats.put("Insert Splits", monitor.getNbrSplit()); - - monitor.reset(); - List found = queryRTree(layer, monitor, stats, false); - indexMaker.verifyStructure(); - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("rtree-bulk-" + splitMode + "/rtree.png"), 7, monitor, found, config.searchMin, config.searchMax); - tx.commit(); - } + } + } + + /* + * Run this manually to generate images of RTree that can be used for animation. + * ffmpeg -f image2 -r 12 -i rtree-single/rtree-%d.png -r 12 -s 1280x960 rtree-single2_12fps.mp4 + */ + @Ignore + public void shouldInsertManyNodesInBulkAndGenerateImagesForAnimation() throws FactoryException, IOException { + IndexTestConfig config = testConfigs.get("medium"); + int blockSize = 1000; + int maxNodeReferences = 10; + String splitMode = RTreeIndex.GREENES_SPLIT; + IndexMaker indexMaker = new RTreeIndexMaker("Coordinates", splitMode, "Bulk", maxNodeReferences, config); + EditableLayer layer = setupLayer(indexMaker); + List nodes = indexMaker.nodes(); + TestStats stats = indexMaker.initStats(blockSize); + + RTreeIndex rtree = (RTreeIndex) layer.getIndex(); + RTreeImageExporter imageExporter; + try (Transaction tx = db.beginTx()) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getTypeFromLayer(tx, layer); + imageExporter = new RTreeImageExporter(layer.getGeometryFactory(), layer.getGeometryEncoder(), + layer.getCoordinateReferenceSystem(tx), featureType, rtree); + imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); + tx.commit(); + } + + TreeMonitor monitor = new TreePrintingMonitor(imageExporter, "bulk", splitMode); + layer.getIndex().addMonitor(monitor); + TimedLogger log = indexMaker.initLogger(); + long start = System.currentTimeMillis(); + for (int i = 0; i < config.totalCount / blockSize; i++) { + List slice = nodes.subList(i * blockSize, i * blockSize + blockSize); + long startIndexing = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + layer.addAll(tx, idsToNodes(tx, slice)); + tx.commit(); + } + log.log(startIndexing, + "Rebuilt: " + monitor.getNbrRebuilt() + ", Splits: " + monitor.getNbrSplit() + ", Cases " + + monitor.getCaseCounts(), (i + 1) * blockSize); + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("rtree-bulk-" + splitMode + "/rtree-" + i + ".png"), 7); + tx.commit(); + } + } + System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to add " + config.totalCount + + " nodes to RTree in bulk"); + stats.setInsertTime(start); + stats.put("Insert Splits", monitor.getNbrSplit()); + + monitor.reset(); + List found = queryRTree(layer, monitor, stats, false); + indexMaker.verifyStructure(); + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("rtree-bulk-" + splitMode + "/rtree.png"), 7, monitor, found, + config.searchMin, config.searchMax); + tx.commit(); + } // debugIndexTree((RTreeIndex) layer.getIndex()); - } + } - @Ignore - public void shouldAccessIndexAfterBulkInsertion() throws Exception { - // Use these two lines if you want to examine the output. + @Ignore + public void shouldAccessIndexAfterBulkInsertion() throws Exception { + // Use these two lines if you want to examine the output. // File dbPath = new File("target/var/BulkTest"); // GraphDatabaseService db = new GraphDatabaseFactory().newEmbeddedDatabase(dbPath.getCanonicalPath()); - SpatialDatabaseService sdbs = spatial(); - EditableLayer layer = getOrCreateSimplePointLayer("Coordinates", "rtree", "lon", "lat"); - - final long numNodes = 100000; - Random rand = new Random(); - - System.out.println("Bulk inserting " + numNodes + " nodes"); - long start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - List coordinateNodes = new ArrayList<>(); - for (int i = 0; i < numNodes; i++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", rand.nextDouble()); - node.setProperty("lon", rand.nextDouble()); - coordinateNodes.add(node); - } - layer.addAll(tx, coordinateNodes); - tx.commit(); - } - System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); - - System.out.println("Searching with spatial.withinDistance"); - start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 - Result result = tx.execute("CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node as malmo"); - int i = 0; - ResourceIterator thing = result.columnAs("malmo"); - while (thing.hasNext()) { - assertNotNull(thing.next()); - i++; - } - assertEquals(i, numNodes); - tx.commit(); - } - System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); - - System.out.println("Searching with spatial.withinDistance and Cypher count"); - start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - String cypher = "CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node\n" + - "RETURN COUNT(node) as count"; - Result result = tx.execute(cypher); + SpatialDatabaseService sdbs = spatial(); + EditableLayer layer = getOrCreateSimplePointLayer("Coordinates", "rtree", "lon", "lat"); + + final long numNodes = 100000; + Random rand = new Random(); + + System.out.println("Bulk inserting " + numNodes + " nodes"); + long start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + List coordinateNodes = new ArrayList<>(); + for (int i = 0; i < numNodes; i++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", rand.nextDouble()); + node.setProperty("lon", rand.nextDouble()); + coordinateNodes.add(node); + } + layer.addAll(tx, coordinateNodes); + tx.commit(); + } + System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); + + System.out.println("Searching with spatial.withinDistance"); + start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 + Result result = tx.execute( + "CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node as malmo"); + int i = 0; + ResourceIterator thing = result.columnAs("malmo"); + while (thing.hasNext()) { + assertNotNull(thing.next()); + i++; + } + assertEquals(i, numNodes); + tx.commit(); + } + System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); + + System.out.println("Searching with spatial.withinDistance and Cypher count"); + start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + String cypher = + "CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node\n" + + "RETURN COUNT(node) as count"; + Result result = tx.execute(cypher); // System.out.println(result.columns().toString()); - Object obj = result.columnAs("count").next(); - assertTrue(obj instanceof Long); - assertEquals((long) ((Long) obj), numNodes); - tx.commit(); - } - System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); - - System.out.println("Searching with pure Cypher"); - start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + - "MATCH (n)-[:RTREE_CHILD*]->(m)-[:RTREE_REFERENCE]->(p)\n" + - "RETURN COUNT(p) as count"; - Result result = tx.execute(cypher); + Object obj = result.columnAs("count").next(); + assertTrue(obj instanceof Long); + assertEquals((long) ((Long) obj), numNodes); + tx.commit(); + } + System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); + + System.out.println("Searching with pure Cypher"); + start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + + "MATCH (n)-[:RTREE_CHILD*]->(m)-[:RTREE_REFERENCE]->(p)\n" + + "RETURN COUNT(p) as count"; + Result result = tx.execute(cypher); // System.out.println(result.columns().toString()); - Object obj = result.columnAs("count").next(); - assertTrue(obj instanceof Long); - assertEquals((long) ((Long) obj), numNodes); - tx.commit(); - } - System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); - } - - @Ignore - public void shouldBuildTreeFromScratch() throws Exception { - //GraphDatabaseService db = this.databases.database("BultTest2"); - GraphDatabaseService db = this.db; - - SpatialDatabaseService sdbs = spatial(); - GeometryEncoder encoder = new SimplePointEncoder(); - - Method decodeEnvelopes = RTreeIndex.class.getDeclaredMethod("decodeEnvelopes", List.class); - decodeEnvelopes.setAccessible(true); - - Method buildRTreeFromScratch = RTreeIndex.class.getDeclaredMethod("buildRtreeFromScratch", Node.class, List.class, double.class); - buildRTreeFromScratch.setAccessible(true); - - Method expectedHeight = RTreeIndex.class.getDeclaredMethod("expectedHeight", double.class, int.class); - expectedHeight.setAccessible(true); - - Random random = new Random(); - random.setSeed(42); - - List range = IntStream.rangeClosed(1, 300).boxed().collect(Collectors.toList()); - //test over the transiton from two to three deep trees - range.addAll(IntStream.rangeClosed(4700, 5000).boxed().collect(Collectors.toList())); - - for (int i : range) { - System.out.println("Building a Tree with " + Integer.toString(i) + " nodes"); - try (Transaction tx = db.beginTx()) { - - RTreeIndex rtree = new RTreeIndex(); - rtree.init(tx, tx.createNode(), encoder, DEFAULT_MAX_NODE_REFERENCES); - List coords = new ArrayList<>(i); - for (int j = 0; j < i; j++) { - Node n = tx.createNode(Label.label("Coordinate")); - n.setProperty(SimplePointEncoder.DEFAULT_X, random.nextDouble() * 90.0); - n.setProperty(SimplePointEncoder.DEFAULT_Y, random.nextDouble() * 90.0); - Geometry geometry = encoder.decodeGeometry(n); - // add BBOX to Node if it's missing - encoder.encodeGeometry(tx, geometry, n); - coords.add(n); - // layer.add(n); - } - - buildRTreeFromScratch.invoke(rtree, rtree.getIndexRoot(tx), decodeEnvelopes.invoke(rtree, coords), 0.7); - RTreeTestUtils testUtils = new RTreeTestUtils(rtree); - - Map results = testUtils.get_height_map(tx, rtree.getIndexRoot(tx)); - assertEquals(1, results.size()); - assertEquals((int) expectedHeight.invoke(rtree, 0.7, coords.size()), results.keySet().iterator().next().intValue()); - assertEquals(results.values().iterator().next().intValue(), coords.size()); - tx.commit(); - } - } - } - - @Ignore - public void shouldPerformRTreeBulkInsertion() throws Exception { - // Use this line if you want to examine the output. - //GraphDatabaseService db = databases.database("BulkTest"); - - SpatialDatabaseService sdbs = spatial(); - int N = 10000; - int Q = 40; - Random random = new Random(); - random.setSeed(42); - // random.setSeed(42); - // leads to: Caused by: org.neo4j.kernel.impl.store.InvalidRecordException: Node[142794,used=false,rel=-1,prop=-1,labels=Inline(0x0:[]),light,secondaryUnitId=-1] not in use - - long totalTimeStart = System.currentTimeMillis(); - for (int j = 1; j < Q + 1; j++) { - System.out.println("BulkLoadingTestRun " + j); - try (Transaction tx = db.beginTx()) { - - - EditableLayer layer = sdbs.getOrCreateSimplePointLayer(tx, "BulkLoader", "rtree", "lon", "lat"); - List coords = new ArrayList<>(N); - for (int i = 0; i < N; i++) { - Node n = tx.createNode(Label.label("Coordinate")); - n.setProperty("lat", random.nextDouble() * 90.0); - n.setProperty("lon", random.nextDouble() * 90.0); - coords.add(n); - // layer.add(n); - } - long time = System.currentTimeMillis(); - - layer.addAll(tx, coords); - System.out.println("********************** time taken to load " + N + " records: " + (System.currentTimeMillis() - time) + "ms"); - - RTreeIndex rtree = (RTreeIndex) layer.getIndex(); - RTreeTestUtils utils = new RTreeTestUtils(rtree); - assertTrue(utils.check_balance(tx, rtree.getIndexRoot(tx))); - - tx.commit(); - } - } - System.out.println("Total Time for " + (N * Q) + " Nodes in " + Q + " Batches of " + N + " is: "); - System.out.println(((System.currentTimeMillis() - totalTimeStart) / 1000) + " seconds"); - - try (Transaction tx = db.beginTx()) { - String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + - "MATCH (n)-[:RTREE_CHILD]->(m)-[:RTREE_CHILD]->(p)-[:RTREE_CHILD]->(s)-[:RTREE_REFERENCE]->(q)\n" + - "RETURN COUNT(q) as count"; - Result result = tx.execute(cypher); - System.out.println(result.columns().toString()); - long count = result.columnAs("count").next(); - assertEquals(N * Q, count); - tx.commit(); - } - - try (Transaction tx = db.beginTx()) { - Layer layer = sdbs.getLayer(tx, "BulkLoader"); - RTreeIndex rtree = (RTreeIndex) layer.getIndex(); - - Node root = rtree.getIndexRoot(tx); - List children = new ArrayList<>(100); - for (Relationship r : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - children.add(r.getEndNode()); - } - RTreeTestUtils utils = new RTreeTestUtils(rtree); - double root_overlap = utils.calculate_overlap(root); - assertTrue(root_overlap < 0.01); //less than one percent - System.out.println("********* Bulk Overlap Percentage" + root_overlap); - - double average_child_overlap = children.stream().mapToDouble(utils::calculate_overlap).average().getAsDouble(); - assertTrue(average_child_overlap < 0.02); - System.out.println("*********** Bulk Average Child Overlap Percentage" + average_child_overlap); - tx.commit(); - } - } - - private List populateSquareTestData(int width) { - GraphDatabaseService db = this.db; - ArrayList nodes = new ArrayList<>(width * width); - for (int i = 0; i < width; i++) { - try (Transaction tx = db.beginTx()) { - for (int j = 0; j < width; j++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", ((double) i / (double) width)); - node.setProperty("lon", ((double) j / (double) width)); - nodes.add(node.getElementId()); - } - tx.commit(); - } - } - java.util.Collections.shuffle(nodes, new Random(8)); - return nodes; - } - - private List populateSquareTestDataHeavy(int width) { - List nodes = populateSquareTestData(width); - Random rand = new Random(42); - - for (int i = 0; i < width / 2; i++) { - try (Transaction tx = db.beginTx()) { - for (int j = 0; j < width / 2; j++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - - node.setProperty("lat", ((double) rand.nextInt(width / 10) / (double) width)); - node.setProperty("lon", ((double) rand.nextInt(width / 10) / (double) width)); - nodes.add(node.getElementId()); - } - tx.commit(); - } - } - java.util.Collections.shuffle(nodes, new Random(8)); - return nodes; - } - - private List populateSquareWithStreets(int width) { - List nodes = new ArrayList<>(); - double squareValue = 0.25; - for (int i = 1; i < 4; i += 2) { - try (Transaction tx = db.beginTx()) { - for (int j = (int) squareValue * width; j < 2 * squareValue * width; j++) { - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", i * squareValue); - node.setProperty("lon", (j + squareValue) / width + squareValue); - nodes.add(node.getElementId()); - Node node2 = tx.createNode(); - node2.addLabel(Label.label("Coordinates")); - node2.setProperty("lat", (j + squareValue) / width + squareValue); - node2.setProperty("lon", i * squareValue); - nodes.add(node2.getElementId()); - - } - tx.commit(); - } - } - for (int i = 0; i < width; i++) { - try (Transaction tx = db.beginTx()) { - - Node node = tx.createNode(); - node.addLabel(Label.label("Coordinates")); - node.setProperty("lat", ((double) i / (double) width)); - node.setProperty("lon", ((double) i / (double) width)); - nodes.add(node.getElementId()); - Node node2 = tx.createNode(); - node2.addLabel(Label.label("Coordinates")); - node2.setProperty("lat", ((double) (width - i) / (double) width)); - node2.setProperty("lon", ((double) i / (double) width)); - nodes.add(node2.getElementId()); - tx.commit(); - } - } - java.util.Collections.shuffle(nodes, new Random(8)); - return nodes; - } - - - private void searchForPos(int numNodes, GraphDatabaseService db) { - System.out.println("Searching with spatial.withinDistance"); - long start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 - Result result = tx.execute("CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node"); - int i = 0; - ResourceIterator thing = result.columnAs("node"); - while (thing.hasNext()) { - assertNotNull(thing.next()); - i++; - } - //assertEquals(i, numNodes); - tx.commit(); - } - System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); - - } - - private List setup(String name, String index, int width) { - long start = System.currentTimeMillis(); - List nodes = populateSquareTestData(width); - System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to create " + (width * width) + " nodes"); - getOrCreateSimplePointLayer(name, index, "lon", "lat"); - return nodes; - } - - private static class NodeWithEnvelope { - Envelope envelope; - Node node; - - NodeWithEnvelope(Node node, Envelope envelope) { - this.node = node; - this.envelope = envelope; - } - } - - private void checkIndexOverlaps(Transaction tx, Layer layer, TestStats stats) { - RTreeIndex index = (RTreeIndex) layer.getIndex(); - Node root = index.getIndexRoot(tx); - ArrayList> nodes = new ArrayList<>(); - nodes.add(new ArrayList<>()); - nodes.get(0).add(new NodeWithEnvelope(root, index.getIndexNodeEnvelope(root))); - do { - ArrayList children = new ArrayList<>(); - for (NodeWithEnvelope parent : nodes.get(nodes.size() - 1)) { - for (Relationship rel : parent.node.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - Node child = rel.getEndNode(); - children.add(new NodeWithEnvelope(child, index.getIndexNodeEnvelope(child))); - } - } - if (children.isEmpty()) { - break; - } else { - nodes.add(children); - } - } while (true); - System.out.println("Comparison of index node areas to root area for " + nodes.size() + " index levels:"); - for (int level = 0; level < nodes.size(); level++) { - double[] overlap = calculateOverlap(nodes.get(0).get(0), nodes.get(level)); - System.out.println("\t" + level + "\t" + nodes.get(level).size() + "\t" + overlap[0] + "\t" + overlap[1]); - stats.put("Leaf Overlap Delta", overlap[0]); - stats.put("Leaf Overlap Ratio", overlap[1]); - } - } - - private double[] calculateOverlap(NodeWithEnvelope root, List nodes) { - double rootArea = root.envelope.getArea(); - double nodesArea = 0.0; - for (NodeWithEnvelope entry : nodes) { - nodesArea += entry.envelope.getArea(); - } - return new double[]{nodesArea - rootArea, nodesArea / rootArea}; - } - - private List queryRTree(Layer layer, TreeMonitor monitor, TestStats stats, boolean assertTouches) { - List nodes = queryIndex(layer, stats); - if (layer.getIndex() instanceof RTreeIndex) { - getRTreeIndexStats((RTreeIndex) layer.getIndex(), monitor, stats, assertTouches, nodes.size()); - } - return nodes; - } - - private List queryRTree(Layer layer, TreeMonitor monitor, TestStats stats) { - List nodes = queryIndex(layer, stats); - if (layer.getIndex() instanceof RTreeIndex) { - getRTreeIndexStats((RTreeIndex) layer.getIndex(), monitor, stats, true, nodes.size()); - } else if (layer.getIndex() instanceof ExplicitIndexBackedPointIndex) { - getExplicitIndexBackedIndexStats((ExplicitIndexBackedPointIndex) layer.getIndex(), stats, true, nodes.size()); - } - return nodes; - } - - private List queryIndex(Layer layer, TestStats stats) { - List nodes; - IndexTestConfig config = stats.config; - long start = System.currentTimeMillis(); - try (Transaction tx = db.beginTx()) { - org.locationtech.jts.geom.Envelope envelope = new org.locationtech.jts.geom.Envelope(config.searchMin, config.searchMax); - nodes = GeoPipeline.startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(envelope)).stream().map(GeoPipeFlow::getGeomNode).collect(Collectors.toList()); - tx.commit(); - } - long countGeometries = nodes.size(); - long queryTime = System.currentTimeMillis() - start; - allStats.add(stats); - stats.put("Query Time (ms)", queryTime); - System.out.println("Took " + queryTime + "ms to find " + countGeometries + " nodes in 4x4 block"); - try (Transaction tx = db.beginTx()) { - int geometrySize = layer.getIndex().count(tx); - stats.put("Indexed", geometrySize); - System.out.println("Index contains " + geometrySize + " geometries"); - } - assertEquals("Expected " + config.expectedGeometries + " nodes to be returned", config.expectedGeometries, countGeometries); - return nodes; - } - - private void getRTreeIndexStats(RTreeIndex index, TreeMonitor monitor, TestStats stats, boolean assertTouches, long countGeometries) { - IndexTestConfig config = stats.config; - int indexTouched = monitor.getCaseCounts().get("Index Does NOT Match"); - int indexMatched = monitor.getCaseCounts().get("Index Matches"); - int touched = monitor.getCaseCounts().get("Geometry Does NOT Match"); - int matched = monitor.getCaseCounts().get("Geometry Matches"); - int indexSize = 0; - try (Transaction tx = db.beginTx()) { - for (Node ignored : index.getAllIndexInternalNodes(tx)) { - indexSize++; - } - tx.commit(); - } - stats.put("Index Size", indexSize); - stats.put("Found", matched); - stats.put("Touched", touched); - stats.put("Index Found", indexMatched); - stats.put("Index Touched", indexTouched); - System.out.println("Searched index of " + indexSize + " nodes in tree of height " + monitor.getHeight()); - System.out.println("Matched " + matched + "/" + touched + " touched nodes (" + (100.0 * matched / touched) + "%)"); - System.out.println("Having matched " + indexMatched + "/" + indexTouched + " touched index nodes (" + (100.0 * indexMatched / indexTouched) + "%)"); - System.out.println("Which means we touched " + indexTouched + "/" + indexSize + " index nodes (" + (100.0 * indexTouched / indexSize) + "%)"); - // Note that due to some crazy GIS spec points on polygon edges are considered to be contained, - // unless the polygon is a rectangle, in which case they are not contained, leading to - // different numbers for expectedGeometries and expectedCount. - // See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 - assertEquals("Expected " + config.expectedCount + " nodes to be matched", config.expectedCount, matched); - int maxNodeReferences = stats.maxNodeReferences; - int maxExpectedGeometriesTouched = matched * maxNodeReferences; - if (countGeometries > 1 && assertTouches) { - assertThat("Should not touch more geometries than " + maxNodeReferences + "*matched", touched, lessThanOrEqualTo(maxExpectedGeometriesTouched)); - int maxExpectedIndexTouched = indexMatched * maxNodeReferences; - assertThat("Should not touch more index nodes than " + maxNodeReferences + "*matched", indexTouched, lessThanOrEqualTo(maxExpectedIndexTouched)); - } - } - - private void getExplicitIndexBackedIndexStats(ExplicitIndexBackedPointIndex index, TestStats stats, boolean assertTouches, long countGeometries) { - IndexTestConfig config = stats.config; - ExplicitIndexBackedMonitor monitor = index.getMonitor(); - long touched = monitor.getHits() + monitor.getMisses(); - long matched = monitor.getHits(); - stats.put("Found", matched); - stats.put("Touched", touched); - System.out.println("Matched " + matched + "/" + touched + " touched nodes (" + (100.0 * matched / touched) + "%)"); - // Note that due to some crazy GIS spec points on polygon edges are considered to be contained, - // unless the polygon is a rectangle, in which case they are not contained, leading to - // different numbers for expectedGeometries and expectedCount. - // See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 - assertEquals("Expected " + config.expectedCount + " nodes to be matched", config.expectedCount, matched); - } - - private class TimedLogger { - String title; - long count; - long gap; - long start; - long previous; - - private TimedLogger(String title, long count) { - this(title, count, 1000); - } - - private TimedLogger(String title, long count, long gap) { - this.title = title; - this.count = count; - this.gap = gap; - this.start = System.currentTimeMillis(); - this.previous = this.start; - System.out.println(title); - } - - private void log(long previous, String line, long number) { - this.previous = previous; - log(line, number); - } - - private void log(String line, long number) { - long current = System.currentTimeMillis(); - if (current - previous > gap) { - double percentage = 100.0 * number / count; - double seconds = (current - start) / 1000.0; - int rate = (int) (number / seconds); - System.out.println("\t" + ((int) percentage) + "%\t" + number + "\t" + seconds + "s\t" + rate + "n/s:\t" + line); - previous = current; - } - } - } - - private void verifyGeohashIndex(Layer layer) { - LayerIndexReader index = layer.getIndex(); - assertTrue("Index should be a geohash index", index instanceof LayerGeohashPointIndex); - } - - private void verifyHilbertIndex(Layer layer) { - LayerIndexReader index = layer.getIndex(); - assertTrue("Index should be a hilbert index", index instanceof LayerHilbertPointIndex); - } - - private void verifyZOrderIndex(Layer layer) { - LayerIndexReader index = layer.getIndex(); - assertTrue("Index should be a Z-Order index", index instanceof LayerZOrderPointIndex); - } - - private void verifyTreeStructure(Layer layer, String splitMode, TestStats stats) { - String layerNodeId; - try (Transaction tx = db.beginTx()) { - Node layerNode = layer.getLayerNode(tx); - layerNodeId = layerNode.getElementId(); - tx.commit(); - } - String queryDepthAndGeometries = - "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + - "MATCH p = (root)-[:RTREE_CHILD*]->(child)-[:RTREE_REFERENCE]->(geometry) " + - "RETURN length(p) as depth, count(*) as geometries"; - - String queryNumChildren = "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + - "MATCH p = (root)-[:RTREE_CHILD*]->(child) " + - "WHERE exists((child)-[:RTREE_REFERENCE]->()) " + - "RETURN length(p) as depth, count (*) as leaves"; - String queryChildrenPerParent = "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + - "MATCH p = (root)-[:RTREE_CHILD*0..]->(parent)-[:RTREE_CHILD]->(child) " + - "WITH parent, count (*) as children RETURN avg(children) as childrenPerParent,min(children) as " + - "MinChildrenPerParent,max(children) as MaxChildrenPerParent"; - String queryChildrenPerParent2 = - "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + - "MATCH p = (root)-[:RTREE_CHILD*0..]->(parent)-[:RTREE_CHILD|RTREE_REFERENCE]->(child) " + - "RETURN parent, length(p) as depth, count (*) as children"; - Map params = Collections.singletonMap("layerNodeId", layerNodeId); - int balanced = 0; - long geometries = 0; - try (Transaction tx = db.beginTx()) { - Result resultDepth = tx.execute(queryDepthAndGeometries, params); - while (resultDepth.hasNext()) { - balanced++; - Map depthMap = resultDepth.next(); - geometries = (long) depthMap.get("geometries"); - System.out.println("Tree depth to all geometries: " + depthMap); - } - } - assertEquals("All geometries should be at the same depth", 1, balanced); - Map leafMap; - try (Transaction tx = db.beginTx()) { - Result resultNumChildren = tx.execute(queryNumChildren, params); - leafMap = resultNumChildren.next(); - System.out.println("Tree depth to all leaves that have geomtries: " + leafMap); - Result resultChildrenPerParent = tx.execute(queryChildrenPerParent, params); - System.out.println("Children per parent: " + resultChildrenPerParent.next()); - } - - int totalNodes = 0; - int underfilledNodes = 0; - int blockSize = Math.max(10, stats.maxNodeReferences) / 10; - Integer[] histogram = new Integer[11]; - try (Transaction tx = db.beginTx()) { - Result resultChildrenPerParent2 = tx.execute(queryChildrenPerParent2, params); - Arrays.fill(histogram, 0); - while (resultChildrenPerParent2.hasNext()) { - Map result = resultChildrenPerParent2.next(); - long children = (long) result.get("children"); - if (children < blockSize * 3) { - underfilledNodes++; - } - totalNodes++; - histogram[(int) children / blockSize]++; - } - } - allStats.add(stats); - stats.put("Underfilled%", 100.0 * underfilledNodes / totalNodes); - - System.out.println("Histogram of child count for " + totalNodes + " index nodes, with " + underfilledNodes + " (" + (100 * underfilledNodes / totalNodes) + "%) underfilled (less than 30% or " + (blockSize * 3) + ")"); - for (int i = 0; i < histogram.length; i++) { - System.out.println("[" + (i * blockSize) + ".." + ((i + 1) * blockSize) + "): " + histogram[i]); - } - if (!splitMode.equals(RTreeIndex.QUADRATIC_SPLIT)) { - assertThat("Expected to have less than 30% of nodes underfilled", underfilledNodes, lessThan(3 * totalNodes / 10)); - } - long leafCountFactor = splitMode.equals(RTreeIndex.QUADRATIC_SPLIT) ? 20 : 2; - long maxLeafCount = leafCountFactor * geometries / stats.maxNodeReferences; - assertThat("In " + splitMode + " we expected leaves to be no more than " + leafCountFactor + "x(geometries/maxNodeReferences)", (long) leafMap.get("leaves"), lessThanOrEqualTo(maxLeafCount)); - try (Transaction tx = db.beginTx()) { - checkIndexOverlaps(tx, layer, stats); - tx.commit(); - } - } - - private void restart() throws IOException { - if (databases != null) { - databases.shutdown(); - } - if (storeDir.exists()) { - System.out.println("Deleting previous database: " + storeDir); - FileUtils.deleteDirectory(storeDir); - } - FileUtils.forceMkdir(storeDir); - databases = new DatabaseManagementServiceBuilder(storeDir.toPath()).build(); - db = databases.database(DEFAULT_DATABASE_NAME); - } - - private void doCleanShutdown() throws IOException { - try { - System.out.println("Shutting down database"); - if (databases != null) { - databases.shutdown(); - } - //TODO: Uncomment this once all tests are stable + Object obj = result.columnAs("count").next(); + assertTrue(obj instanceof Long); + assertEquals((long) ((Long) obj), numNodes); + tx.commit(); + } + System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); + } + + @Ignore + public void shouldBuildTreeFromScratch() throws Exception { + //GraphDatabaseService db = this.databases.database("BultTest2"); + GraphDatabaseService db = this.db; + + SpatialDatabaseService sdbs = spatial(); + GeometryEncoder encoder = new SimplePointEncoder(); + + Method decodeEnvelopes = RTreeIndex.class.getDeclaredMethod("decodeEnvelopes", List.class); + decodeEnvelopes.setAccessible(true); + + Method buildRTreeFromScratch = RTreeIndex.class.getDeclaredMethod("buildRtreeFromScratch", Node.class, + List.class, double.class); + buildRTreeFromScratch.setAccessible(true); + + Method expectedHeight = RTreeIndex.class.getDeclaredMethod("expectedHeight", double.class, int.class); + expectedHeight.setAccessible(true); + + Random random = new Random(); + random.setSeed(42); + + List range = IntStream.rangeClosed(1, 300).boxed().collect(Collectors.toList()); + //test over the transiton from two to three deep trees + range.addAll(IntStream.rangeClosed(4700, 5000).boxed().collect(Collectors.toList())); + + for (int i : range) { + System.out.println("Building a Tree with " + Integer.toString(i) + " nodes"); + try (Transaction tx = db.beginTx()) { + + RTreeIndex rtree = new RTreeIndex(); + rtree.init(tx, tx.createNode(), encoder, DEFAULT_MAX_NODE_REFERENCES); + List coords = new ArrayList<>(i); + for (int j = 0; j < i; j++) { + Node n = tx.createNode(Label.label("Coordinate")); + n.setProperty(SimplePointEncoder.DEFAULT_X, random.nextDouble() * 90.0); + n.setProperty(SimplePointEncoder.DEFAULT_Y, random.nextDouble() * 90.0); + Geometry geometry = encoder.decodeGeometry(n); + // add BBOX to Node if it's missing + encoder.encodeGeometry(tx, geometry, n); + coords.add(n); + // layer.add(n); + } + + buildRTreeFromScratch.invoke(rtree, rtree.getIndexRoot(tx), decodeEnvelopes.invoke(rtree, coords), 0.7); + RTreeTestUtils testUtils = new RTreeTestUtils(rtree); + + Map results = testUtils.get_height_map(tx, rtree.getIndexRoot(tx)); + assertEquals(1, results.size()); + assertEquals((int) expectedHeight.invoke(rtree, 0.7, coords.size()), + results.keySet().iterator().next().intValue()); + assertEquals(results.values().iterator().next().intValue(), coords.size()); + tx.commit(); + } + } + } + + @Ignore + public void shouldPerformRTreeBulkInsertion() throws Exception { + // Use this line if you want to examine the output. + //GraphDatabaseService db = databases.database("BulkTest"); + + SpatialDatabaseService sdbs = spatial(); + int N = 10000; + int Q = 40; + Random random = new Random(); + random.setSeed(42); + // random.setSeed(42); + // leads to: Caused by: org.neo4j.kernel.impl.store.InvalidRecordException: Node[142794,used=false,rel=-1,prop=-1,labels=Inline(0x0:[]),light,secondaryUnitId=-1] not in use + + long totalTimeStart = System.currentTimeMillis(); + for (int j = 1; j < Q + 1; j++) { + System.out.println("BulkLoadingTestRun " + j); + try (Transaction tx = db.beginTx()) { + + EditableLayer layer = sdbs.getOrCreateSimplePointLayer(tx, "BulkLoader", "rtree", "lon", "lat"); + List coords = new ArrayList<>(N); + for (int i = 0; i < N; i++) { + Node n = tx.createNode(Label.label("Coordinate")); + n.setProperty("lat", random.nextDouble() * 90.0); + n.setProperty("lon", random.nextDouble() * 90.0); + coords.add(n); + // layer.add(n); + } + long time = System.currentTimeMillis(); + + layer.addAll(tx, coords); + System.out.println( + "********************** time taken to load " + N + " records: " + (System.currentTimeMillis() + - time) + "ms"); + + RTreeIndex rtree = (RTreeIndex) layer.getIndex(); + RTreeTestUtils utils = new RTreeTestUtils(rtree); + assertTrue(utils.check_balance(tx, rtree.getIndexRoot(tx))); + + tx.commit(); + } + } + System.out.println("Total Time for " + (N * Q) + " Nodes in " + Q + " Batches of " + N + " is: "); + System.out.println(((System.currentTimeMillis() - totalTimeStart) / 1000) + " seconds"); + + try (Transaction tx = db.beginTx()) { + String cypher = "MATCH ()-[:RTREE_ROOT]->(n)\n" + + "MATCH (n)-[:RTREE_CHILD]->(m)-[:RTREE_CHILD]->(p)-[:RTREE_CHILD]->(s)-[:RTREE_REFERENCE]->(q)\n" + + "RETURN COUNT(q) as count"; + Result result = tx.execute(cypher); + System.out.println(result.columns().toString()); + long count = result.columnAs("count").next(); + assertEquals(N * Q, count); + tx.commit(); + } + + try (Transaction tx = db.beginTx()) { + Layer layer = sdbs.getLayer(tx, "BulkLoader"); + RTreeIndex rtree = (RTreeIndex) layer.getIndex(); + + Node root = rtree.getIndexRoot(tx); + List children = new ArrayList<>(100); + for (Relationship r : root.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + children.add(r.getEndNode()); + } + RTreeTestUtils utils = new RTreeTestUtils(rtree); + double root_overlap = utils.calculate_overlap(root); + assertTrue(root_overlap < 0.01); //less than one percent + System.out.println("********* Bulk Overlap Percentage" + root_overlap); + + double average_child_overlap = children.stream().mapToDouble(utils::calculate_overlap).average() + .getAsDouble(); + assertTrue(average_child_overlap < 0.02); + System.out.println("*********** Bulk Average Child Overlap Percentage" + average_child_overlap); + tx.commit(); + } + } + + private List populateSquareTestData(int width) { + GraphDatabaseService db = this.db; + ArrayList nodes = new ArrayList<>(width * width); + for (int i = 0; i < width; i++) { + try (Transaction tx = db.beginTx()) { + for (int j = 0; j < width; j++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", ((double) i / (double) width)); + node.setProperty("lon", ((double) j / (double) width)); + nodes.add(node.getElementId()); + } + tx.commit(); + } + } + java.util.Collections.shuffle(nodes, new Random(8)); + return nodes; + } + + private List populateSquareTestDataHeavy(int width) { + List nodes = populateSquareTestData(width); + Random rand = new Random(42); + + for (int i = 0; i < width / 2; i++) { + try (Transaction tx = db.beginTx()) { + for (int j = 0; j < width / 2; j++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + + node.setProperty("lat", ((double) rand.nextInt(width / 10) / (double) width)); + node.setProperty("lon", ((double) rand.nextInt(width / 10) / (double) width)); + nodes.add(node.getElementId()); + } + tx.commit(); + } + } + java.util.Collections.shuffle(nodes, new Random(8)); + return nodes; + } + + private List populateSquareWithStreets(int width) { + List nodes = new ArrayList<>(); + double squareValue = 0.25; + for (int i = 1; i < 4; i += 2) { + try (Transaction tx = db.beginTx()) { + for (int j = (int) squareValue * width; j < 2 * squareValue * width; j++) { + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", i * squareValue); + node.setProperty("lon", (j + squareValue) / width + squareValue); + nodes.add(node.getElementId()); + Node node2 = tx.createNode(); + node2.addLabel(Label.label("Coordinates")); + node2.setProperty("lat", (j + squareValue) / width + squareValue); + node2.setProperty("lon", i * squareValue); + nodes.add(node2.getElementId()); + + } + tx.commit(); + } + } + for (int i = 0; i < width; i++) { + try (Transaction tx = db.beginTx()) { + + Node node = tx.createNode(); + node.addLabel(Label.label("Coordinates")); + node.setProperty("lat", ((double) i / (double) width)); + node.setProperty("lon", ((double) i / (double) width)); + nodes.add(node.getElementId()); + Node node2 = tx.createNode(); + node2.addLabel(Label.label("Coordinates")); + node2.setProperty("lat", ((double) (width - i) / (double) width)); + node2.setProperty("lon", ((double) i / (double) width)); + nodes.add(node2.getElementId()); + tx.commit(); + } + } + java.util.Collections.shuffle(nodes, new Random(8)); + return nodes; + } + + + private void searchForPos(int numNodes, GraphDatabaseService db) { + System.out.println("Searching with spatial.withinDistance"); + long start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { // 'points',{longitude:15.0,latitude:60.0},100 + Result result = tx.execute( + "CALL spatial.withinDistance('Coordinates',{longitude:0.5, latitude:0.5},1000.0) yield node"); + int i = 0; + ResourceIterator thing = result.columnAs("node"); + while (thing.hasNext()) { + assertNotNull(thing.next()); + i++; + } + //assertEquals(i, numNodes); + tx.commit(); + } + System.out.println("\t" + (System.currentTimeMillis() - start) + "ms"); + + } + + private List setup(String name, String index, int width) { + long start = System.currentTimeMillis(); + List nodes = populateSquareTestData(width); + System.out.println( + "Took " + (System.currentTimeMillis() - start) + "ms to create " + (width * width) + " nodes"); + getOrCreateSimplePointLayer(name, index, "lon", "lat"); + return nodes; + } + + private static class NodeWithEnvelope { + + Envelope envelope; + Node node; + + NodeWithEnvelope(Node node, Envelope envelope) { + this.node = node; + this.envelope = envelope; + } + } + + private void checkIndexOverlaps(Transaction tx, Layer layer, TestStats stats) { + RTreeIndex index = (RTreeIndex) layer.getIndex(); + Node root = index.getIndexRoot(tx); + ArrayList> nodes = new ArrayList<>(); + nodes.add(new ArrayList<>()); + nodes.get(0).add(new NodeWithEnvelope(root, index.getIndexNodeEnvelope(root))); + do { + ArrayList children = new ArrayList<>(); + for (NodeWithEnvelope parent : nodes.get(nodes.size() - 1)) { + for (Relationship rel : parent.node.getRelationships(Direction.OUTGOING, + RTreeRelationshipTypes.RTREE_CHILD)) { + Node child = rel.getEndNode(); + children.add(new NodeWithEnvelope(child, index.getIndexNodeEnvelope(child))); + } + } + if (children.isEmpty()) { + break; + } else { + nodes.add(children); + } + } while (true); + System.out.println("Comparison of index node areas to root area for " + nodes.size() + " index levels:"); + for (int level = 0; level < nodes.size(); level++) { + double[] overlap = calculateOverlap(nodes.get(0).get(0), nodes.get(level)); + System.out.println("\t" + level + "\t" + nodes.get(level).size() + "\t" + overlap[0] + "\t" + overlap[1]); + stats.put("Leaf Overlap Delta", overlap[0]); + stats.put("Leaf Overlap Ratio", overlap[1]); + } + } + + private double[] calculateOverlap(NodeWithEnvelope root, List nodes) { + double rootArea = root.envelope.getArea(); + double nodesArea = 0.0; + for (NodeWithEnvelope entry : nodes) { + nodesArea += entry.envelope.getArea(); + } + return new double[]{nodesArea - rootArea, nodesArea / rootArea}; + } + + private List queryRTree(Layer layer, TreeMonitor monitor, TestStats stats, boolean assertTouches) { + List nodes = queryIndex(layer, stats); + if (layer.getIndex() instanceof RTreeIndex) { + getRTreeIndexStats((RTreeIndex) layer.getIndex(), monitor, stats, assertTouches, nodes.size()); + } + return nodes; + } + + private List queryRTree(Layer layer, TreeMonitor monitor, TestStats stats) { + List nodes = queryIndex(layer, stats); + if (layer.getIndex() instanceof RTreeIndex) { + getRTreeIndexStats((RTreeIndex) layer.getIndex(), monitor, stats, true, nodes.size()); + } else if (layer.getIndex() instanceof ExplicitIndexBackedPointIndex) { + getExplicitIndexBackedIndexStats((ExplicitIndexBackedPointIndex) layer.getIndex(), stats, true, + nodes.size()); + } + return nodes; + } + + private List queryIndex(Layer layer, TestStats stats) { + List nodes; + IndexTestConfig config = stats.config; + long start = System.currentTimeMillis(); + try (Transaction tx = db.beginTx()) { + org.locationtech.jts.geom.Envelope envelope = new org.locationtech.jts.geom.Envelope(config.searchMin, + config.searchMax); + nodes = GeoPipeline.startWithinSearch(tx, layer, layer.getGeometryFactory().toGeometry(envelope)).stream() + .map(GeoPipeFlow::getGeomNode).collect(Collectors.toList()); + tx.commit(); + } + long countGeometries = nodes.size(); + long queryTime = System.currentTimeMillis() - start; + allStats.add(stats); + stats.put("Query Time (ms)", queryTime); + System.out.println("Took " + queryTime + "ms to find " + countGeometries + " nodes in 4x4 block"); + try (Transaction tx = db.beginTx()) { + int geometrySize = layer.getIndex().count(tx); + stats.put("Indexed", geometrySize); + System.out.println("Index contains " + geometrySize + " geometries"); + } + assertEquals("Expected " + config.expectedGeometries + " nodes to be returned", config.expectedGeometries, + countGeometries); + return nodes; + } + + private void getRTreeIndexStats(RTreeIndex index, TreeMonitor monitor, TestStats stats, boolean assertTouches, + long countGeometries) { + IndexTestConfig config = stats.config; + int indexTouched = monitor.getCaseCounts().get("Index Does NOT Match"); + int indexMatched = monitor.getCaseCounts().get("Index Matches"); + int touched = monitor.getCaseCounts().get("Geometry Does NOT Match"); + int matched = monitor.getCaseCounts().get("Geometry Matches"); + int indexSize = 0; + try (Transaction tx = db.beginTx()) { + for (Node ignored : index.getAllIndexInternalNodes(tx)) { + indexSize++; + } + tx.commit(); + } + stats.put("Index Size", indexSize); + stats.put("Found", matched); + stats.put("Touched", touched); + stats.put("Index Found", indexMatched); + stats.put("Index Touched", indexTouched); + System.out.println("Searched index of " + indexSize + " nodes in tree of height " + monitor.getHeight()); + System.out.println( + "Matched " + matched + "/" + touched + " touched nodes (" + (100.0 * matched / touched) + "%)"); + System.out.println( + "Having matched " + indexMatched + "/" + indexTouched + " touched index nodes (" + (100.0 * indexMatched + / indexTouched) + "%)"); + System.out.println( + "Which means we touched " + indexTouched + "/" + indexSize + " index nodes (" + (100.0 * indexTouched + / indexSize) + "%)"); + // Note that due to some crazy GIS spec points on polygon edges are considered to be contained, + // unless the polygon is a rectangle, in which case they are not contained, leading to + // different numbers for expectedGeometries and expectedCount. + // See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 + assertEquals("Expected " + config.expectedCount + " nodes to be matched", config.expectedCount, matched); + int maxNodeReferences = stats.maxNodeReferences; + int maxExpectedGeometriesTouched = matched * maxNodeReferences; + if (countGeometries > 1 && assertTouches) { + assertThat("Should not touch more geometries than " + maxNodeReferences + "*matched", touched, + lessThanOrEqualTo(maxExpectedGeometriesTouched)); + int maxExpectedIndexTouched = indexMatched * maxNodeReferences; + assertThat("Should not touch more index nodes than " + maxNodeReferences + "*matched", indexTouched, + lessThanOrEqualTo(maxExpectedIndexTouched)); + } + } + + private void getExplicitIndexBackedIndexStats(ExplicitIndexBackedPointIndex index, TestStats stats, + boolean assertTouches, long countGeometries) { + IndexTestConfig config = stats.config; + ExplicitIndexBackedMonitor monitor = index.getMonitor(); + long touched = monitor.getHits() + monitor.getMisses(); + long matched = monitor.getHits(); + stats.put("Found", matched); + stats.put("Touched", touched); + System.out.println( + "Matched " + matched + "/" + touched + " touched nodes (" + (100.0 * matched / touched) + "%)"); + // Note that due to some crazy GIS spec points on polygon edges are considered to be contained, + // unless the polygon is a rectangle, in which case they are not contained, leading to + // different numbers for expectedGeometries and expectedCount. + // See https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/predicate/RectangleContains.java#L70 + assertEquals("Expected " + config.expectedCount + " nodes to be matched", config.expectedCount, matched); + } + + private class TimedLogger { + + String title; + long count; + long gap; + long start; + long previous; + + private TimedLogger(String title, long count) { + this(title, count, 1000); + } + + private TimedLogger(String title, long count, long gap) { + this.title = title; + this.count = count; + this.gap = gap; + this.start = System.currentTimeMillis(); + this.previous = this.start; + System.out.println(title); + } + + private void log(long previous, String line, long number) { + this.previous = previous; + log(line, number); + } + + private void log(String line, long number) { + long current = System.currentTimeMillis(); + if (current - previous > gap) { + double percentage = 100.0 * number / count; + double seconds = (current - start) / 1000.0; + int rate = (int) (number / seconds); + System.out.println( + "\t" + ((int) percentage) + "%\t" + number + "\t" + seconds + "s\t" + rate + "n/s:\t" + line); + previous = current; + } + } + } + + private void verifyGeohashIndex(Layer layer) { + LayerIndexReader index = layer.getIndex(); + assertTrue("Index should be a geohash index", index instanceof LayerGeohashPointIndex); + } + + private void verifyHilbertIndex(Layer layer) { + LayerIndexReader index = layer.getIndex(); + assertTrue("Index should be a hilbert index", index instanceof LayerHilbertPointIndex); + } + + private void verifyZOrderIndex(Layer layer) { + LayerIndexReader index = layer.getIndex(); + assertTrue("Index should be a Z-Order index", index instanceof LayerZOrderPointIndex); + } + + private void verifyTreeStructure(Layer layer, String splitMode, TestStats stats) { + String layerNodeId; + try (Transaction tx = db.beginTx()) { + Node layerNode = layer.getLayerNode(tx); + layerNodeId = layerNode.getElementId(); + tx.commit(); + } + String queryDepthAndGeometries = + "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + + "MATCH p = (root)-[:RTREE_CHILD*]->(child)-[:RTREE_REFERENCE]->(geometry) " + + "RETURN length(p) as depth, count(*) as geometries"; + + String queryNumChildren = "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + + "MATCH p = (root)-[:RTREE_CHILD*]->(child) " + + "WHERE exists((child)-[:RTREE_REFERENCE]->()) " + + "RETURN length(p) as depth, count (*) as leaves"; + String queryChildrenPerParent = + "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + + "MATCH p = (root)-[:RTREE_CHILD*0..]->(parent)-[:RTREE_CHILD]->(child) " + + "WITH parent, count (*) as children RETURN avg(children) as childrenPerParent,min(children) as " + + + "MinChildrenPerParent,max(children) as MaxChildrenPerParent"; + String queryChildrenPerParent2 = + "MATCH (layer)-[:RTREE_ROOT]->(root) WHERE elementId(layer)=$layerNodeId WITH root " + + "MATCH p = (root)-[:RTREE_CHILD*0..]->(parent)-[:RTREE_CHILD|RTREE_REFERENCE]->(child) " + + "RETURN parent, length(p) as depth, count (*) as children"; + Map params = Collections.singletonMap("layerNodeId", layerNodeId); + int balanced = 0; + long geometries = 0; + try (Transaction tx = db.beginTx()) { + Result resultDepth = tx.execute(queryDepthAndGeometries, params); + while (resultDepth.hasNext()) { + balanced++; + Map depthMap = resultDepth.next(); + geometries = (long) depthMap.get("geometries"); + System.out.println("Tree depth to all geometries: " + depthMap); + } + } + assertEquals("All geometries should be at the same depth", 1, balanced); + Map leafMap; + try (Transaction tx = db.beginTx()) { + Result resultNumChildren = tx.execute(queryNumChildren, params); + leafMap = resultNumChildren.next(); + System.out.println("Tree depth to all leaves that have geomtries: " + leafMap); + Result resultChildrenPerParent = tx.execute(queryChildrenPerParent, params); + System.out.println("Children per parent: " + resultChildrenPerParent.next()); + } + + int totalNodes = 0; + int underfilledNodes = 0; + int blockSize = Math.max(10, stats.maxNodeReferences) / 10; + Integer[] histogram = new Integer[11]; + try (Transaction tx = db.beginTx()) { + Result resultChildrenPerParent2 = tx.execute(queryChildrenPerParent2, params); + Arrays.fill(histogram, 0); + while (resultChildrenPerParent2.hasNext()) { + Map result = resultChildrenPerParent2.next(); + long children = (long) result.get("children"); + if (children < blockSize * 3) { + underfilledNodes++; + } + totalNodes++; + histogram[(int) children / blockSize]++; + } + } + allStats.add(stats); + stats.put("Underfilled%", 100.0 * underfilledNodes / totalNodes); + + System.out.println( + "Histogram of child count for " + totalNodes + " index nodes, with " + underfilledNodes + " (" + ( + 100 * underfilledNodes / totalNodes) + "%) underfilled (less than 30% or " + (blockSize * 3) + + ")"); + for (int i = 0; i < histogram.length; i++) { + System.out.println("[" + (i * blockSize) + ".." + ((i + 1) * blockSize) + "): " + histogram[i]); + } + if (!splitMode.equals(RTreeIndex.QUADRATIC_SPLIT)) { + assertThat("Expected to have less than 30% of nodes underfilled", underfilledNodes, + lessThan(3 * totalNodes / 10)); + } + long leafCountFactor = splitMode.equals(RTreeIndex.QUADRATIC_SPLIT) ? 20 : 2; + long maxLeafCount = leafCountFactor * geometries / stats.maxNodeReferences; + assertThat("In " + splitMode + " we expected leaves to be no more than " + leafCountFactor + + "x(geometries/maxNodeReferences)", (long) leafMap.get("leaves"), lessThanOrEqualTo(maxLeafCount)); + try (Transaction tx = db.beginTx()) { + checkIndexOverlaps(tx, layer, stats); + tx.commit(); + } + } + + private void restart() throws IOException { + if (databases != null) { + databases.shutdown(); + } + if (storeDir.exists()) { + System.out.println("Deleting previous database: " + storeDir); + FileUtils.deleteDirectory(storeDir); + } + FileUtils.forceMkdir(storeDir); + databases = new DatabaseManagementServiceBuilder(storeDir.toPath()).build(); + db = databases.database(DEFAULT_DATABASE_NAME); + } + + private void doCleanShutdown() throws IOException { + try { + System.out.println("Shutting down database"); + if (databases != null) { + databases.shutdown(); + } + //TODO: Uncomment this once all tests are stable // FileUtils.deleteDirectory(storeDir); - } finally { - databases = null; - db = null; - } - } - - private static class TestStats { - private final IndexTestConfig config; - private final String insertMode; - private final int dataSize; - private final int blockSize; - private final String splitMode; - private final int maxNodeReferences; - static LinkedHashSet knownKeys = new LinkedHashSet<>(); - private final HashMap data = new HashMap<>(); - - private TestStats(IndexTestConfig config, String insertMode, String splitMode, int blockSize, int maxNodeReferences) { - this.config = config; - this.insertMode = insertMode; - this.dataSize = config.width * config.width; - this.blockSize = blockSize; - this.splitMode = splitMode; - this.maxNodeReferences = maxNodeReferences; - } - - private void setInsertTime(long start) { - long current = System.currentTimeMillis(); - double seconds = (current - start) / 1000.0; - int rate = (int) (dataSize / seconds); - put("Insert Time (s)", seconds); - put("Insert Rate (n/s)", rate); - } - - public void put(String key, Object value) { - knownKeys.add(key); - data.put(key, value); - } - - public void get(String key) { - data.get(key); - } - - private static String[] headerArray() { - return new String[]{"Size Name", "Insert Mode", "Split Mode", "Data Width", "Data Size", "Block Size", "Max Node References"}; - } - - private Object[] fieldArray() { - return new Object[]{config.name, insertMode, splitMode, config.width, dataSize, blockSize, maxNodeReferences}; - } - - private static List headerList() { - ArrayList fieldList = new ArrayList<>(); - fieldList.addAll(Arrays.asList(headerArray())); - fieldList.addAll(knownKeys); - return fieldList; - } - - private List asList() { - ArrayList fieldList = new ArrayList<>(); - fieldList.addAll(Arrays.asList(fieldArray())); - fieldList.addAll(knownKeys.stream().map(data::get).map(v -> (v == null) ? "" : v).collect(Collectors.toList())); - return fieldList; - } - - private static String headerString() { - return String.join("\t", headerList()); - } - - @Override - public String toString() { - return asList().stream().map(Object::toString).collect(Collectors.joining("\t")); - } - } - - private static final LinkedHashSet allStats = new LinkedHashSet<>(); - - @AfterClass - public static void afterClass() { - System.out.println("\n\nComposite stats for " + allStats.size() + " tests run"); - System.out.println(TestStats.headerString()); - for (TestStats stats : allStats) { - System.out.println(stats); - } - } + } finally { + databases = null; + db = null; + } + } + + private static class TestStats { + + private final IndexTestConfig config; + private final String insertMode; + private final int dataSize; + private final int blockSize; + private final String splitMode; + private final int maxNodeReferences; + static LinkedHashSet knownKeys = new LinkedHashSet<>(); + private final HashMap data = new HashMap<>(); + + private TestStats(IndexTestConfig config, String insertMode, String splitMode, int blockSize, + int maxNodeReferences) { + this.config = config; + this.insertMode = insertMode; + this.dataSize = config.width * config.width; + this.blockSize = blockSize; + this.splitMode = splitMode; + this.maxNodeReferences = maxNodeReferences; + } + + private void setInsertTime(long start) { + long current = System.currentTimeMillis(); + double seconds = (current - start) / 1000.0; + int rate = (int) (dataSize / seconds); + put("Insert Time (s)", seconds); + put("Insert Rate (n/s)", rate); + } + + public void put(String key, Object value) { + knownKeys.add(key); + data.put(key, value); + } + + public void get(String key) { + data.get(key); + } + + private static String[] headerArray() { + return new String[]{"Size Name", "Insert Mode", "Split Mode", "Data Width", "Data Size", "Block Size", + "Max Node References"}; + } + + private Object[] fieldArray() { + return new Object[]{config.name, insertMode, splitMode, config.width, dataSize, blockSize, + maxNodeReferences}; + } + + private static List headerList() { + ArrayList fieldList = new ArrayList<>(); + fieldList.addAll(Arrays.asList(headerArray())); + fieldList.addAll(knownKeys); + return fieldList; + } + + private List asList() { + ArrayList fieldList = new ArrayList<>(); + fieldList.addAll(Arrays.asList(fieldArray())); + fieldList.addAll( + knownKeys.stream().map(data::get).map(v -> (v == null) ? "" : v).collect(Collectors.toList())); + return fieldList; + } + + private static String headerString() { + return String.join("\t", headerList()); + } + + @Override + public String toString() { + return asList().stream().map(Object::toString).collect(Collectors.joining("\t")); + } + } + + private static final LinkedHashSet allStats = new LinkedHashSet<>(); + + @AfterClass + public static void afterClass() { + System.out.println("\n\nComposite stats for " + allStats.size() + " tests run"); + System.out.println(TestStats.headerString()); + for (TestStats stats : allStats) { + System.out.println(stats); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/RTreeTestUtils.java b/src/test/java/org/neo4j/gis/spatial/RTreeTestUtils.java index 91192b08c..ac5a111dd 100644 --- a/src/test/java/org/neo4j/gis/spatial/RTreeTestUtils.java +++ b/src/test/java/org/neo4j/gis/spatial/RTreeTestUtils.java @@ -1,102 +1,108 @@ package org.neo4j.gis.spatial; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.neo4j.gis.spatial.rtree.Envelope; import org.neo4j.gis.spatial.rtree.RTreeIndex; import org.neo4j.gis.spatial.rtree.RTreeRelationshipTypes; -import org.neo4j.graphdb.*; - -import java.util.*; -import java.util.stream.Collectors; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; /** * Created by Philip Stephens on 12/11/2016. */ public class RTreeTestUtils { - /** - * This class contain functions which can be used to test the integrity of the Rtree. - */ - - private final RTreeIndex rtree; - - public RTreeTestUtils(RTreeIndex rtree) { - this.rtree = rtree; - } - - public static double one_d_overlap(double a1, double a2, double b1, double b2) { - return Double.max( - Double.min(a2, b2) - Double.max(a1, b1), - 0.0 - ); - } - - public static double compute_overlap(Envelope a, Envelope b) { - return one_d_overlap(a.getMinX(), a.getMaxX(), b.getMinX(), b.getMaxX()) * - one_d_overlap(a.getMinY(), a.getMaxY(), b.getMinY(), b.getMaxY()); - } - - public double calculate_overlap(Node child) { - - Envelope parent = rtree.getIndexNodeEnvelope(child); - List children = new ArrayList(); - - for (Relationship r : child.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { - children.add(rtree.getIndexNodeEnvelope(r.getEndNode())); - } - children.sort(Comparator.comparing(Envelope::getMinX, Double::compare)); - double total_overlap = 0.0; - List activeNodes = new LinkedList<>(); - - - for (Envelope x : children) { - activeNodes = activeNodes - .stream() - .filter(envelope -> envelope.getMaxX() < x.getMinX()) - .collect(Collectors.toList()); - total_overlap += activeNodes.stream().mapToDouble(envelope -> compute_overlap(x, envelope)).sum(); - activeNodes.add(x); - } - - return total_overlap / parent.getArea(); - - } - - public Map get_height_map(Transaction tx, Node root) { - String id = root.getElementId(); - - - String cypher = "MATCH p = (root) -[:RTREE_CHILD*0..] ->(child) -[:RTREE_REFERENCE]->(leaf)\n" + - " WHERE elementId(root) = " + id + "\n" + - " RETURN length(p) as depth, count (*) as freq"; - Result result = tx.execute(cypher); - - int i = 0; - Map map = new HashMap<>(); - while (result.hasNext()) { - Map r = result.next(); - map.put((Long) r.get("depth"), (Long) r.get("freq")); - i++; - } - return map; - } - - public boolean check_balance(Transaction tx, Node root) { - String id = root.getElementId(); - - - String cypher = "MATCH p = (root) -[:RTREE_CHILD*0..] ->(child) -[:RTREE_REFERENCE]->(leaf)\n" + - " WHERE elementId(root) = " + id + "\n" + - " RETURN length(p) as depth, count (*) as freq"; - Result result = tx.execute(cypher); - - int i = 0; - while (result.hasNext()) { - Map r = result.next(); - System.out.println(r.get("depth").toString() + " : " + r.get("freq")); - i++; - } - return i == 1; - - } + + /** + * This class contain functions which can be used to test the integrity of the Rtree. + */ + + private final RTreeIndex rtree; + + public RTreeTestUtils(RTreeIndex rtree) { + this.rtree = rtree; + } + + public static double one_d_overlap(double a1, double a2, double b1, double b2) { + return Double.max( + Double.min(a2, b2) - Double.max(a1, b1), + 0.0 + ); + } + + public static double compute_overlap(Envelope a, Envelope b) { + return one_d_overlap(a.getMinX(), a.getMaxX(), b.getMinX(), b.getMaxX()) * + one_d_overlap(a.getMinY(), a.getMaxY(), b.getMinY(), b.getMaxY()); + } + + public double calculate_overlap(Node child) { + + Envelope parent = rtree.getIndexNodeEnvelope(child); + List children = new ArrayList(); + + for (Relationship r : child.getRelationships(Direction.OUTGOING, RTreeRelationshipTypes.RTREE_CHILD)) { + children.add(rtree.getIndexNodeEnvelope(r.getEndNode())); + } + children.sort(Comparator.comparing(Envelope::getMinX, Double::compare)); + double total_overlap = 0.0; + List activeNodes = new LinkedList<>(); + + for (Envelope x : children) { + activeNodes = activeNodes + .stream() + .filter(envelope -> envelope.getMaxX() < x.getMinX()) + .collect(Collectors.toList()); + total_overlap += activeNodes.stream().mapToDouble(envelope -> compute_overlap(x, envelope)).sum(); + activeNodes.add(x); + } + + return total_overlap / parent.getArea(); + + } + + public Map get_height_map(Transaction tx, Node root) { + String id = root.getElementId(); + + String cypher = "MATCH p = (root) -[:RTREE_CHILD*0..] ->(child) -[:RTREE_REFERENCE]->(leaf)\n" + + " WHERE elementId(root) = " + id + "\n" + + " RETURN length(p) as depth, count (*) as freq"; + Result result = tx.execute(cypher); + + int i = 0; + Map map = new HashMap<>(); + while (result.hasNext()) { + Map r = result.next(); + map.put((Long) r.get("depth"), (Long) r.get("freq")); + i++; + } + return map; + } + + public boolean check_balance(Transaction tx, Node root) { + String id = root.getElementId(); + + String cypher = "MATCH p = (root) -[:RTREE_CHILD*0..] ->(child) -[:RTREE_REFERENCE]->(leaf)\n" + + " WHERE elementId(root) = " + id + "\n" + + " RETURN length(p) as depth, count (*) as freq"; + Result result = tx.execute(cypher); + + int i = 0; + while (result.hasNext()) { + Map r = result.next(); + System.out.println(r.get("depth").toString() + " : " + r.get("freq")); + i++; + } + return i == 1; + + } } diff --git a/src/test/java/org/neo4j/gis/spatial/SpatialIndexPerformanceProxy.java b/src/test/java/org/neo4j/gis/spatial/SpatialIndexPerformanceProxy.java index 2c09614c8..d3d38bc5a 100644 --- a/src/test/java/org/neo4j/gis/spatial/SpatialIndexPerformanceProxy.java +++ b/src/test/java/org/neo4j/gis/spatial/SpatialIndexPerformanceProxy.java @@ -20,7 +20,7 @@ package org.neo4j.gis.spatial; import java.util.Map; - +import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.index.LayerIndexReader; import org.neo4j.gis.spatial.rtree.Envelope; @@ -28,7 +28,6 @@ import org.neo4j.gis.spatial.rtree.TreeMonitor; import org.neo4j.gis.spatial.rtree.filter.SearchFilter; import org.neo4j.gis.spatial.rtree.filter.SearchResults; -import org.neo4j.gis.spatial.filter.SearchRecords; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; @@ -37,101 +36,103 @@ */ public class SpatialIndexPerformanceProxy implements LayerIndexReader { - private final LayerIndexReader spatialIndex; - - public SpatialIndexPerformanceProxy(LayerIndexReader spatialIndex) { - this.spatialIndex = spatialIndex; - } - - @Override - public void init(Transaction tx, IndexManager indexManager, Layer layer) { - if (layer != getLayer()) throw new IllegalArgumentException("Cannot change layer associated with this index"); - } - - @Override - public Layer getLayer() { - return spatialIndex.getLayer(); - } - - @Override - public boolean isEmpty(Transaction tx) { - long start = System.currentTimeMillis(); - boolean result = spatialIndex.isEmpty(tx); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(count): " + (stop - start) + "ms"); - return result; - } - - @Override - public int count(Transaction tx) { - long start = System.currentTimeMillis(); - int count = spatialIndex.count(tx); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(count): " + (stop - start) + "ms"); - return count; - } - - public Iterable getAllGeometryNodes(Transaction tx) { - return spatialIndex.getAllIndexedNodes(tx); - } - - @Override - public EnvelopeDecoder getEnvelopeDecoder() { - return spatialIndex.getEnvelopeDecoder(); - } - - @Override - public Envelope getBoundingBox(Transaction tx) { - long start = System.currentTimeMillis(); - Envelope result = spatialIndex.getBoundingBox(tx); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(getBoundingBox()): " + (stop - start) + "ms"); - return result; - } - - @Override - public boolean isNodeIndexed(Transaction tx, String nodeId) { - long start = System.currentTimeMillis(); - boolean result = spatialIndex.isNodeIndexed(tx, nodeId); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(isNodeIndexed(" + nodeId + ")): " + (stop - start) + "ms"); - return result; - } - - @Override - public Iterable getAllIndexedNodes(Transaction tx) { - long start = System.currentTimeMillis(); - Iterable result = spatialIndex.getAllIndexedNodes(tx); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(getAllIndexedNodes()): " + (stop - start) + "ms"); - return result; - } - - @Override - public SearchResults searchIndex(Transaction tx, SearchFilter filter) { - long start = System.currentTimeMillis(); - SearchResults results = spatialIndex.searchIndex(tx, filter); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(executeSearch(" + filter + ")): " + (stop - start) + "ms"); - return results; - } - - @Override - public void addMonitor(TreeMonitor monitor) { - - } - - @Override - public void configure(Map config) { - - } - - @Override - public SearchRecords search(Transaction tx, SearchFilter filter) { - long start = System.currentTimeMillis(); - SearchRecords results = spatialIndex.search(tx, filter); - long stop = System.currentTimeMillis(); - System.out.println("# exec time(executeSearch(" + filter + ")): " + (stop - start) + "ms"); - return results; - } + private final LayerIndexReader spatialIndex; + + public SpatialIndexPerformanceProxy(LayerIndexReader spatialIndex) { + this.spatialIndex = spatialIndex; + } + + @Override + public void init(Transaction tx, IndexManager indexManager, Layer layer) { + if (layer != getLayer()) { + throw new IllegalArgumentException("Cannot change layer associated with this index"); + } + } + + @Override + public Layer getLayer() { + return spatialIndex.getLayer(); + } + + @Override + public boolean isEmpty(Transaction tx) { + long start = System.currentTimeMillis(); + boolean result = spatialIndex.isEmpty(tx); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(count): " + (stop - start) + "ms"); + return result; + } + + @Override + public int count(Transaction tx) { + long start = System.currentTimeMillis(); + int count = spatialIndex.count(tx); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(count): " + (stop - start) + "ms"); + return count; + } + + public Iterable getAllGeometryNodes(Transaction tx) { + return spatialIndex.getAllIndexedNodes(tx); + } + + @Override + public EnvelopeDecoder getEnvelopeDecoder() { + return spatialIndex.getEnvelopeDecoder(); + } + + @Override + public Envelope getBoundingBox(Transaction tx) { + long start = System.currentTimeMillis(); + Envelope result = spatialIndex.getBoundingBox(tx); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(getBoundingBox()): " + (stop - start) + "ms"); + return result; + } + + @Override + public boolean isNodeIndexed(Transaction tx, String nodeId) { + long start = System.currentTimeMillis(); + boolean result = spatialIndex.isNodeIndexed(tx, nodeId); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(isNodeIndexed(" + nodeId + ")): " + (stop - start) + "ms"); + return result; + } + + @Override + public Iterable getAllIndexedNodes(Transaction tx) { + long start = System.currentTimeMillis(); + Iterable result = spatialIndex.getAllIndexedNodes(tx); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(getAllIndexedNodes()): " + (stop - start) + "ms"); + return result; + } + + @Override + public SearchResults searchIndex(Transaction tx, SearchFilter filter) { + long start = System.currentTimeMillis(); + SearchResults results = spatialIndex.searchIndex(tx, filter); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(executeSearch(" + filter + ")): " + (stop - start) + "ms"); + return results; + } + + @Override + public void addMonitor(TreeMonitor monitor) { + + } + + @Override + public void configure(Map config) { + + } + + @Override + public SearchRecords search(Transaction tx, SearchFilter filter) { + long start = System.currentTimeMillis(); + SearchRecords results = spatialIndex.search(tx, filter); + long stop = System.currentTimeMillis(); + System.out.println("# exec time(executeSearch(" + filter + ")): " + (stop - start) + "ms"); + return results; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestDynamicLayers.java b/src/test/java/org/neo4j/gis/spatial/TestDynamicLayers.java index 5a0a08fe5..227d9ef6f 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestDynamicLayers.java +++ b/src/test/java/org/neo4j/gis/spatial/TestDynamicLayers.java @@ -19,6 +19,13 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import org.geotools.api.data.DataSourceException; import org.geotools.api.data.DataStore; import org.geotools.data.neo4j.Neo4jSpatialDataStore; @@ -37,249 +44,260 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - public class TestDynamicLayers extends Neo4jTestCase implements Constants { - @Test - public void testShapefileExport_Map1() throws Exception { - runShapefileExport("map.osm"); - } + @Test + public void testShapefileExport_Map1() throws Exception { + runShapefileExport("map.osm"); + } - @Test - public void testShapefileExport_Map2() throws Exception { - runShapefileExport("map2.osm"); - } + @Test + public void testShapefileExport_Map2() throws Exception { + runShapefileExport("map2.osm"); + } - @Test - public void testImageExport_HighwayShp() throws Exception { - runDynamicShapefile("highway.shp"); - } + @Test + public void testImageExport_HighwayShp() throws Exception { + runDynamicShapefile("highway.shp"); + } - @SuppressWarnings("SameParameterValue") - private void runDynamicShapefile(String shpFile) throws Exception { - printDatabaseStats(); - loadTestShpData(shpFile); - checkLayer(shpFile); - printDatabaseStats(); + @SuppressWarnings("SameParameterValue") + private void runDynamicShapefile(String shpFile) throws Exception { + printDatabaseStats(); + loadTestShpData(shpFile); + checkLayer(shpFile); + printDatabaseStats(); - // Define dynamic layers - ArrayList layers = new ArrayList<>(); - try (Transaction tx = graphDb().beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - DynamicLayer shpLayer = spatial.asDynamicLayer(tx, spatial.getLayer(tx, shpFile)); - layers.add(shpLayer.addLayerConfig(tx, "CQL0-highway", GTYPE_GEOMETRY, "highway is not null")); - layers.add(shpLayer.addLayerConfig(tx, "CQL1-highway", GTYPE_POINT, "geometryType(the_geom) = 'MultiLineString'")); - layers.add(shpLayer.addLayerConfig(tx, "CQL2-highway", GTYPE_LINESTRING, "highway is not null and geometryType(the_geom) = 'MultiLineString'")); - layers.add(shpLayer.addLayerConfig(tx, "CQL3-residential", GTYPE_MULTILINESTRING, "highway = 'residential'")); - layers.add(shpLayer.addLayerConfig(tx, "CQL4-nameV", GTYPE_LINESTRING, "name is not null and name like 'V%'")); - layers.add(shpLayer.addLayerConfig(tx, "CQL5-nameS", GTYPE_LINESTRING, "name is not null and name like 'S%'")); - layers.add(shpLayer.addLayerConfig(tx, "CQL6-nameABC", GTYPE_LINESTRING, "name like 'A%' or name like 'B%' or name like 'B%'")); - layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "residential", GTYPE_MULTILINESTRING)); - layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "path", GTYPE_MULTILINESTRING)); - layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "track", GTYPE_MULTILINESTRING)); - assertEquals(layers.size() + 1, shpLayer.getLayerNames(tx).size()); - tx.commit(); - } - try (Transaction tx = graphDb().beginTx()) { + // Define dynamic layers + ArrayList layers = new ArrayList<>(); + try (Transaction tx = graphDb().beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + DynamicLayer shpLayer = spatial.asDynamicLayer(tx, spatial.getLayer(tx, shpFile)); + layers.add(shpLayer.addLayerConfig(tx, "CQL0-highway", GTYPE_GEOMETRY, "highway is not null")); + layers.add(shpLayer.addLayerConfig(tx, "CQL1-highway", GTYPE_POINT, + "geometryType(the_geom) = 'MultiLineString'")); + layers.add(shpLayer.addLayerConfig(tx, "CQL2-highway", GTYPE_LINESTRING, + "highway is not null and geometryType(the_geom) = 'MultiLineString'")); + layers.add( + shpLayer.addLayerConfig(tx, "CQL3-residential", GTYPE_MULTILINESTRING, "highway = 'residential'")); + layers.add( + shpLayer.addLayerConfig(tx, "CQL4-nameV", GTYPE_LINESTRING, "name is not null and name like 'V%'")); + layers.add( + shpLayer.addLayerConfig(tx, "CQL5-nameS", GTYPE_LINESTRING, "name is not null and name like 'S%'")); + layers.add(shpLayer.addLayerConfig(tx, "CQL6-nameABC", GTYPE_LINESTRING, + "name like 'A%' or name like 'B%' or name like 'B%'")); + layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "residential", GTYPE_MULTILINESTRING)); + layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "path", GTYPE_MULTILINESTRING)); + layers.add(shpLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "track", GTYPE_MULTILINESTRING)); + assertEquals(layers.size() + 1, shpLayer.getLayerNames(tx).size()); + tx.commit(); + } + try (Transaction tx = graphDb().beginTx()) { - // Now export the layers to files - // First prepare the SHP and PNG exporters - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - imageExporter.setExportDir("target/export/" + shpFile); - imageExporter.setZoom(3.0); - imageExporter.setOffset(-0.05, -0.05); - imageExporter.setSize(1024, 768); + // Now export the layers to files + // First prepare the SHP and PNG exporters + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + imageExporter.setExportDir("target/export/" + shpFile); + imageExporter.setZoom(3.0); + imageExporter.setOffset(-0.05, -0.05); + imageExporter.setSize(1024, 768); - // Now loop through all dynamic layers and export them to images, - // where possible. Layers will multiple geometries cannot be exported - // and we take note of how many times that happens - for (Layer layer : layers) { - // for (Layer layer : new Layer[] {}) { - checkIndexAndFeatureCount(layer); - imageExporter.saveLayerImage(layer.getName(), null); - } - tx.commit(); - } - } + // Now loop through all dynamic layers and export them to images, + // where possible. Layers will multiple geometries cannot be exported + // and we take note of how many times that happens + for (Layer layer : layers) { + // for (Layer layer : new Layer[] {}) { + checkIndexAndFeatureCount(layer); + imageExporter.saveLayerImage(layer.getName(), null); + } + tx.commit(); + } + } - private void runShapefileExport(String osmFile) throws Exception { - // TODO: Consider merits of using dependency data in target/osm, - // downloaded by maven, as done in TestSpatial, versus the test data - // commited to source code as done here - printDatabaseStats(); - loadTestOsmData(osmFile); - Envelope bbox = checkLayer(osmFile); - printDatabaseStats(); - //bbox.expandBy(-0.1); - bbox = scale(bbox, 0.2); + private void runShapefileExport(String osmFile) throws Exception { + // TODO: Consider merits of using dependency data in target/osm, + // downloaded by maven, as done in TestSpatial, versus the test data + // commited to source code as done here + printDatabaseStats(); + loadTestOsmData(osmFile); + Envelope bbox = checkLayer(osmFile); + printDatabaseStats(); + //bbox.expandBy(-0.1); + bbox = scale(bbox, 0.2); - // Define dynamic layers - ArrayList layers = new ArrayList<>(); - try (Transaction tx = graphDb().beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osmFile); - LinearRing ring = osmLayer.getGeometryFactory().createLinearRing( - new Coordinate[]{new Coordinate(bbox.getMinX(), bbox.getMinY()), new Coordinate(bbox.getMinX(), bbox.getMaxY()), - new Coordinate(bbox.getMaxX(), bbox.getMaxY()), new Coordinate(bbox.getMaxX(), bbox.getMinY()), - new Coordinate(bbox.getMinX(), bbox.getMinY())}); - Polygon polygon = osmLayer.getGeometryFactory().createPolygon(ring, null); - layers.add(osmLayer.addLayerConfig(tx, "CQL1-highway", GTYPE_LINESTRING, "highway is not null and geometryType(the_geom) = 'LineString'")); - layers.add(osmLayer.addLayerConfig(tx, "CQL2-residential", GTYPE_LINESTRING, "highway = 'residential' and geometryType(the_geom) = 'LineString'")); - layers.add(osmLayer.addLayerConfig(tx, "CQL3-natural", GTYPE_POLYGON, "natural is not null and geometryType(the_geom) = 'Polygon'")); - layers.add(osmLayer.addLayerConfig(tx, "CQL4-water", GTYPE_POLYGON, "natural = 'water' and geometryType(the_geom) = 'Polygon'")); - layers.add(osmLayer.addLayerConfig(tx, "CQL5-bbox", GTYPE_GEOMETRY, "BBOX(the_geom, " + toCoordinateText(bbox) + ")")); - layers.add(osmLayer.addLayerConfig(tx, "CQL6-bbox-polygon", GTYPE_GEOMETRY, "within(the_geom, POLYGON((" - + toCoordinateText(polygon.getCoordinates()) + ")))")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "primary")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "secondary")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "tertiary")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_LINESTRING, "highway=*")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_LINESTRING, "highway=footway, bicycle=yes")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway=*, bicycle=yes")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "residential")); - layers.add(osmLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "residential", GTYPE_LINESTRING)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "footway")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "cycleway")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "track")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "path")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "unclassified")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "amenity", "parking", GTYPE_POLYGON)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "railway", null)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", null)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "waterway", null)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "building", null, GTYPE_POLYGON)); - layers.add(osmLayer.addCQLDynamicLayerOnAttribute(tx, "building", null, GTYPE_POLYGON)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", null, GTYPE_GEOMETRY)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "water", GTYPE_POLYGON)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "wood", GTYPE_POLYGON)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "coastline")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "beach")); - layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_POLYGON)); - layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_POINT)); - layers.add(osmLayer.addCQLDynamicLayerOnGeometryType(tx, GTYPE_POLYGON)); - layers.add(osmLayer.addCQLDynamicLayerOnGeometryType(tx, GTYPE_POINT)); - assertEquals(layers.size() + 1, osmLayer.getLayerNames(tx).size()); - tx.commit(); - } - // Now export the layers to files - // First prepare the SHP and PNG exporters - ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); - shpExporter.setExportDir("target/export/" + osmFile); - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - imageExporter.setExportDir("target/export/" + osmFile); - imageExporter.setZoom(3.0); - imageExporter.setOffset(-0.05, -0.05); - imageExporter.setSize(1024, 768); - // imageExporter.saveLayerImage("highway", null); - // imageExporter.saveLayerImage(osmLayer.getName(), "neo.sld.xml"); + // Define dynamic layers + ArrayList layers = new ArrayList<>(); + try (Transaction tx = graphDb().beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osmFile); + LinearRing ring = osmLayer.getGeometryFactory().createLinearRing( + new Coordinate[]{new Coordinate(bbox.getMinX(), bbox.getMinY()), + new Coordinate(bbox.getMinX(), bbox.getMaxY()), + new Coordinate(bbox.getMaxX(), bbox.getMaxY()), + new Coordinate(bbox.getMaxX(), bbox.getMinY()), + new Coordinate(bbox.getMinX(), bbox.getMinY())}); + Polygon polygon = osmLayer.getGeometryFactory().createPolygon(ring, null); + layers.add(osmLayer.addLayerConfig(tx, "CQL1-highway", GTYPE_LINESTRING, + "highway is not null and geometryType(the_geom) = 'LineString'")); + layers.add(osmLayer.addLayerConfig(tx, "CQL2-residential", GTYPE_LINESTRING, + "highway = 'residential' and geometryType(the_geom) = 'LineString'")); + layers.add(osmLayer.addLayerConfig(tx, "CQL3-natural", GTYPE_POLYGON, + "natural is not null and geometryType(the_geom) = 'Polygon'")); + layers.add(osmLayer.addLayerConfig(tx, "CQL4-water", GTYPE_POLYGON, + "natural = 'water' and geometryType(the_geom) = 'Polygon'")); + layers.add(osmLayer.addLayerConfig(tx, "CQL5-bbox", GTYPE_GEOMETRY, + "BBOX(the_geom, " + toCoordinateText(bbox) + ")")); + layers.add(osmLayer.addLayerConfig(tx, "CQL6-bbox-polygon", GTYPE_GEOMETRY, "within(the_geom, POLYGON((" + + toCoordinateText(polygon.getCoordinates()) + ")))")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "primary")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "secondary")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "tertiary")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_LINESTRING, "highway=*")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_LINESTRING, "highway=footway, bicycle=yes")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway=*, bicycle=yes")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "residential")); + layers.add(osmLayer.addCQLDynamicLayerOnAttribute(tx, "highway", "residential", GTYPE_LINESTRING)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "footway")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "cycleway")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "track")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "path")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", "unclassified")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "amenity", "parking", GTYPE_POLYGON)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "railway", null)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "highway", null)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "waterway", null)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "building", null, GTYPE_POLYGON)); + layers.add(osmLayer.addCQLDynamicLayerOnAttribute(tx, "building", null, GTYPE_POLYGON)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", null, GTYPE_GEOMETRY)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "water", GTYPE_POLYGON)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "wood", GTYPE_POLYGON)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "coastline")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, "natural", "beach")); + layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_POLYGON)); + layers.add(osmLayer.addSimpleDynamicLayer(tx, GTYPE_POINT)); + layers.add(osmLayer.addCQLDynamicLayerOnGeometryType(tx, GTYPE_POLYGON)); + layers.add(osmLayer.addCQLDynamicLayerOnGeometryType(tx, GTYPE_POINT)); + assertEquals(layers.size() + 1, osmLayer.getLayerNames(tx).size()); + tx.commit(); + } + // Now export the layers to files + // First prepare the SHP and PNG exporters + ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); + shpExporter.setExportDir("target/export/" + osmFile); + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + imageExporter.setExportDir("target/export/" + osmFile); + imageExporter.setZoom(3.0); + imageExporter.setOffset(-0.05, -0.05); + imageExporter.setSize(1024, 768); + // imageExporter.saveLayerImage("highway", null); + // imageExporter.saveLayerImage(osmLayer.getName(), "neo.sld.xml"); - // Now loop through all dynamic layers and export them to shapefiles, - // where possible. Layers will multiple geometries cannot be exported - // and we take note of how many times that happens - int countMultiGeometryLayers = 0; - int countMultiGeometryExceptions = 0; - for (Layer layer : layers) { - Integer geometryType; - try (Transaction tx = graphDb().beginTx()) { - geometryType = layer.getGeometryType(tx); - if (layer.getGeometryType(tx) == GTYPE_GEOMETRY) { - countMultiGeometryLayers++; - } - tx.commit(); - } - checkIndexAndFeatureCount(layer); - try { - imageExporter.saveLayerImage(layer.getName(), null); - shpExporter.exportLayer(layer.getName()); - } catch (Exception e) { - if (e instanceof DataSourceException && e.getMessage().contains("geom.Geometry")) { - System.out.println("Got geometry exception on layer with geometry[" - + SpatialDatabaseService.convertGeometryTypeToName(geometryType) + "]: " - + e.getMessage()); - countMultiGeometryExceptions++; - } else { - throw e; - } - } - } - assertEquals(countMultiGeometryLayers, countMultiGeometryExceptions, - "Mismatching number of data source exceptions and raw geometry layers"); - } + // Now loop through all dynamic layers and export them to shapefiles, + // where possible. Layers will multiple geometries cannot be exported + // and we take note of how many times that happens + int countMultiGeometryLayers = 0; + int countMultiGeometryExceptions = 0; + for (Layer layer : layers) { + Integer geometryType; + try (Transaction tx = graphDb().beginTx()) { + geometryType = layer.getGeometryType(tx); + if (layer.getGeometryType(tx) == GTYPE_GEOMETRY) { + countMultiGeometryLayers++; + } + tx.commit(); + } + checkIndexAndFeatureCount(layer); + try { + imageExporter.saveLayerImage(layer.getName(), null); + shpExporter.exportLayer(layer.getName()); + } catch (Exception e) { + if (e instanceof DataSourceException && e.getMessage().contains("geom.Geometry")) { + System.out.println("Got geometry exception on layer with geometry[" + + SpatialDatabaseService.convertGeometryTypeToName(geometryType) + "]: " + + e.getMessage()); + countMultiGeometryExceptions++; + } else { + throw e; + } + } + } + assertEquals(countMultiGeometryLayers, countMultiGeometryExceptions, + "Mismatching number of data source exceptions and raw geometry layers"); + } - @SuppressWarnings("SameParameterValue") - private Envelope scale(Envelope bbox, double fraction) { - double xoff = bbox.getWidth(0) * (1.0 - fraction) / 2.0; - double yoff = bbox.getWidth(1) * (1.0 - fraction) / 2.0; - return new Envelope(bbox.getMinX() + xoff, bbox.getMaxX() - xoff, bbox.getMinY() + yoff, bbox.getMaxY() - yoff); - } + @SuppressWarnings("SameParameterValue") + private Envelope scale(Envelope bbox, double fraction) { + double xoff = bbox.getWidth(0) * (1.0 - fraction) / 2.0; + double yoff = bbox.getWidth(1) * (1.0 - fraction) / 2.0; + return new Envelope(bbox.getMinX() + xoff, bbox.getMaxX() - xoff, bbox.getMinY() + yoff, bbox.getMaxY() - yoff); + } - private String toCoordinateText(Coordinate[] coordinates) { - StringBuilder sb = new StringBuilder(); - for (Coordinate c : coordinates) { - if (sb.length() > 0) - sb.append(", "); - sb.append(c.x).append(" ").append(c.y); - } - return sb.toString(); - } + private String toCoordinateText(Coordinate[] coordinates) { + StringBuilder sb = new StringBuilder(); + for (Coordinate c : coordinates) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(c.x).append(" ").append(c.y); + } + return sb.toString(); + } - private String toCoordinateText(Envelope bbox) { - return "" + bbox.getMinX() + ", " + bbox.getMinY() + ", " + bbox.getMaxX() + ", " + bbox.getMaxY(); - } + private String toCoordinateText(Envelope bbox) { + return "" + bbox.getMinX() + ", " + bbox.getMinY() + ", " + bbox.getMaxX() + ", " + bbox.getMaxY(); + } - private void checkIndexAndFeatureCount(Layer layer) throws IOException { - try (Transaction tx = graphDb().beginTx()) { - if (layer.getIndex().count(tx) < 1) { - System.out.println("Warning: index count zero: " + layer.getName()); - } - System.out.println("Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); - tx.commit(); - } - DataStore store = new Neo4jSpatialDataStore(graphDb()); - try (Transaction tx = graphDb().beginTx()) { - SimpleFeatureCollection features = store.getFeatureSource(layer.getName()).getFeatures(); - System.out.println("Layer '" + layer.getName() + "' has " + features.size() + " features"); - assertEquals(layer.getIndex().count(tx), features.size(), "FeatureCollection.size for layer '" + layer.getName() + "' not the same as index count"); - tx.commit(); - } - } + private void checkIndexAndFeatureCount(Layer layer) throws IOException { + try (Transaction tx = graphDb().beginTx()) { + if (layer.getIndex().count(tx) < 1) { + System.out.println("Warning: index count zero: " + layer.getName()); + } + System.out.println( + "Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); + tx.commit(); + } + DataStore store = new Neo4jSpatialDataStore(graphDb()); + try (Transaction tx = graphDb().beginTx()) { + SimpleFeatureCollection features = store.getFeatureSource(layer.getName()).getFeatures(); + System.out.println("Layer '" + layer.getName() + "' has " + features.size() + " features"); + assertEquals(layer.getIndex().count(tx), features.size(), + "FeatureCollection.size for layer '" + layer.getName() + "' not the same as index count"); + tx.commit(); + } + } - private void loadTestOsmData(String layerName) throws Exception { - System.out.println("\n=== Loading layer " + layerName + " from " + layerName + " ==="); - OSMImporter importer = new OSMImporter(layerName); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(graphDb(), layerName, 1000); - importer.reIndex(graphDb(), 1000); - } + private void loadTestOsmData(String layerName) throws Exception { + System.out.println("\n=== Loading layer " + layerName + " from " + layerName + " ==="); + OSMImporter importer = new OSMImporter(layerName); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(graphDb(), layerName, 1000); + importer.reIndex(graphDb(), 1000); + } - private void loadTestShpData(String layerName) throws IOException { - String shpPath = "shp" + File.separator + layerName; - System.out.println("\n=== Loading layer " + layerName + " from " + shpPath + " ==="); - ShapefileImporter importer = new ShapefileImporter(graphDb(), new NullListener(), 1000); - importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); - } + private void loadTestShpData(String layerName) throws IOException { + String shpPath = "shp" + File.separator + layerName; + System.out.println("\n=== Loading layer " + layerName + " from " + shpPath + " ==="); + ShapefileImporter importer = new ShapefileImporter(graphDb(), new NullListener(), 1000); + importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); + } - private Envelope checkLayer(String layerName) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - Layer layer; - try (Transaction tx = graphDb().beginTx()) { - layer = spatial.getLayer(tx, layerName); - } - assertNotNull(layer.getIndex(), "Layer index should not be null"); - Envelope bbox; - try (Transaction tx = graphDb().beginTx()) { - bbox = layer.getIndex().getBoundingBox(tx); - } - assertNotNull(bbox, "Layer index envelope should not be null"); - System.out.println("Layer has bounding box: " + bbox); - Neo4jTestUtils.debugIndexTree(graphDb(), layerName); - return bbox; - } + private Envelope checkLayer(String layerName) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + Layer layer; + try (Transaction tx = graphDb().beginTx()) { + layer = spatial.getLayer(tx, layerName); + } + assertNotNull(layer.getIndex(), "Layer index should not be null"); + Envelope bbox; + try (Transaction tx = graphDb().beginTx()) { + bbox = layer.getIndex().getBoundingBox(tx); + } + assertNotNull(bbox, "Layer index envelope should not be null"); + System.out.println("Layer has bounding box: " + bbox); + Neo4jTestUtils.debugIndexTree(graphDb(), layerName); + return bbox; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestIntersectsPathQueries.java b/src/test/java/org/neo4j/gis/spatial/TestIntersectsPathQueries.java index 6b04fba88..c986edd64 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestIntersectsPathQueries.java +++ b/src/test/java/org/neo4j/gis/spatial/TestIntersectsPathQueries.java @@ -19,6 +19,23 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; @@ -39,310 +56,320 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.Map.Entry; -import java.util.function.Function; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - public class TestIntersectsPathQueries { - /** - * This test case is designed to capture the conditions described in the bug - * report at https://github.com/neo4j/spatial/issues/112. The report claims - * that intersects searches on large sets of points are very low. This test - * We should write a test case that demonstrates the difference in performance - * between three approaches. - *
    - *
  • Iterating over set of points
  • - *
  • build a MultiPoint and search with that
  • - *
  • build a LineString and search with that
  • - *
- * We would expect the LineString and MultiPoint to perform very much better - * than the set iteration, especially if the sample Geometry sets (roads) is - * very large, and the set of test points quite large. - */ - @Test - public void testPointSetGeoptimaIntersection() throws InterruptedException { - String osmPath = "albert/osm/massachusetts.highway.osm"; - String shpPath = "albert/shp/massachusetts_highway.shp"; - String dbRoot = "target/geoptima"; - String dbName = "massachusetts.highway.db"; - String layerName = "massachusetts"; - String tracePath = "albert/locations_input.txt"; - File dbDir = new File(dbRoot, "data/databases/" + dbName); - if (dbDir.exists() && dbDir.isDirectory()) { - System.out.println("Found database[" + dbName + "] - running test directly on existing database"); - runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); - } else if ((new File(shpPath)).exists()) { - System.out.println("No database[" + dbName + "] but found shp[" + shpPath + "] - importing before running test"); - importShapefileDatabase(shpPath, dbRoot, dbName, layerName); - runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); - } else if ((new File(osmPath)).exists()) { - System.out.println("No database[" + dbName + "] but found osm[" + osmPath + "] - importing before running test"); - importOSMDatabase(osmPath, dbRoot, dbName, layerName); - runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); - } else { - System.out.println("No database[" + dbName + "] or osm[" + osmPath + "] - cannot run test"); - } - } + /** + * This test case is designed to capture the conditions described in the bug + * report at https://github.com/neo4j/spatial/issues/112. The report claims + * that intersects searches on large sets of points are very low. This test + * We should write a test case that demonstrates the difference in performance + * between three approaches. + *
    + *
  • Iterating over set of points
  • + *
  • build a MultiPoint and search with that
  • + *
  • build a LineString and search with that
  • + *
+ * We would expect the LineString and MultiPoint to perform very much better + * than the set iteration, especially if the sample Geometry sets (roads) is + * very large, and the set of test points quite large. + */ + @Test + public void testPointSetGeoptimaIntersection() throws InterruptedException { + String osmPath = "albert/osm/massachusetts.highway.osm"; + String shpPath = "albert/shp/massachusetts_highway.shp"; + String dbRoot = "target/geoptima"; + String dbName = "massachusetts.highway.db"; + String layerName = "massachusetts"; + String tracePath = "albert/locations_input.txt"; + File dbDir = new File(dbRoot, "data/databases/" + dbName); + if (dbDir.exists() && dbDir.isDirectory()) { + System.out.println("Found database[" + dbName + "] - running test directly on existing database"); + runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); + } else if ((new File(shpPath)).exists()) { + System.out.println( + "No database[" + dbName + "] but found shp[" + shpPath + "] - importing before running test"); + importShapefileDatabase(shpPath, dbRoot, dbName, layerName); + runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); + } else if ((new File(osmPath)).exists()) { + System.out.println( + "No database[" + dbName + "] but found osm[" + osmPath + "] - importing before running test"); + importOSMDatabase(osmPath, dbRoot, dbName, layerName); + runTestPointSetGeoptimaIntersection(tracePath, dbRoot, dbName, layerName, false); + } else { + System.out.println("No database[" + dbName + "] or osm[" + osmPath + "] - cannot run test"); + } + } + + private void importShapefileDatabase(String shpPath, String dbRoot, String dbName, String layerName) { + withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { + ShapefileImporter importer = new ShapefileImporter(graphDb, new ConsoleListener(), 10000, true); + importer.setFilterEnvelope(makeFilterEnvelope()); + try { + importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); + return null; + } catch (IOException e) { + return e; + } + }); + } - private void importShapefileDatabase(String shpPath, String dbRoot, String dbName, String layerName) { - withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { - ShapefileImporter importer = new ShapefileImporter(graphDb, new ConsoleListener(), 10000, true); - importer.setFilterEnvelope(makeFilterEnvelope()); - try { - importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); - return null; - } catch (IOException e) { - return e; - } - }); - } + private Envelope makeFilterEnvelope() { + Envelope filterEnvelope = new Envelope(); + filterEnvelope.expandToInclude(new Coordinate(-71.00, 42.10)); + filterEnvelope.expandToInclude(new Coordinate(-71.70, 42.50)); + return filterEnvelope; + } - private Envelope makeFilterEnvelope() { - Envelope filterEnvelope = new Envelope(); - filterEnvelope.expandToInclude(new Coordinate(-71.00, 42.10)); - filterEnvelope.expandToInclude(new Coordinate(-71.70, 42.50)); - return filterEnvelope; - } + private void importOSMDatabase(String osmPath, String dbRoot, String dbName, String layerName) + throws InterruptedException { + // TODO: Port to batch inserter in `github.com/neo4j-contrib/osm` project + OSMImporter importer = new OSMImporter(layerName, new ConsoleListener(), makeFilterEnvelope()); + withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { + try { + importer.importFile(graphDb, osmPath, 10000); + return null; + } catch (Exception e) { + return e; + } + }); + // Weird hack to force GC on large loads + long start = System.currentTimeMillis(); + if (System.currentTimeMillis() - start > 300000) { + for (int i = 0; i < 3; i++) { + System.gc(); + Thread.sleep(1000); + } + } + withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { + importer.reIndex(graphDb, 10000, false); + try { + TestOSMImport.checkOSMLayer(graphDb, layerName); + return null; + } catch (Exception e) { + return e; + } + }); + } - private void importOSMDatabase(String osmPath, String dbRoot, String dbName, String layerName) throws InterruptedException { - // TODO: Port to batch inserter in `github.com/neo4j-contrib/osm` project - OSMImporter importer = new OSMImporter(layerName, new ConsoleListener(), makeFilterEnvelope()); - withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { - try { - importer.importFile(graphDb, osmPath, 10000); - return null; - } catch (Exception e) { - return e; - } - }); - // Weird hack to force GC on large loads - long start = System.currentTimeMillis(); - if (System.currentTimeMillis() - start > 300000) { - for (int i = 0; i < 3; i++) { - System.gc(); - Thread.sleep(1000); - } - } - withDatabase(dbRoot, dbName, Neo4jTestCase.LARGE_CONFIG, graphDb -> { - importer.reIndex(graphDb, 10000, false); - try { - TestOSMImport.checkOSMLayer(graphDb, layerName); - return null; - } catch (Exception e) { - return e; - } - }); - } + private static class Performance { - private static class Performance { - long start; - long duration; - String name; - Collection results; - int count; + long start; + long duration; + String name; + Collection results; + int count; - private Performance(String name) { - this.name = name; - start(); - } + private Performance(String name) { + this.name = name; + start(); + } - public void start() { - this.start = System.currentTimeMillis(); - } + public void start() { + this.start = System.currentTimeMillis(); + } - public void stop(int count) { - this.count = count; - } + public void stop(int count) { + this.count = count; + } - public void stop(Collection results) { - this.duration = System.currentTimeMillis() - start; - this.results = results; - this.count = results.size(); - } + public void stop(Collection results) { + this.duration = System.currentTimeMillis() - start; + this.results = results; + this.count = results.size(); + } - private double overlaps(Collection original) { - if (results == null) { - return 0.0; - } else if (original.size() < results.size()) { - return fractionOf(results, original); - } else { - return fractionOf(original, results); - } - } + private double overlaps(Collection original) { + if (results == null) { + return 0.0; + } else if (original.size() < results.size()) { + return fractionOf(results, original); + } else { + return fractionOf(original, results); + } + } - private double fractionOf(Collection subset, Collection set) { - HashSet all = new HashSet<>(set); - int count = 0; - for (Node node : subset) { - if (all.contains(node)) { - count++; - } - } - return ((double) count) / ((double) set.size()); - } + private double fractionOf(Collection subset, Collection set) { + HashSet all = new HashSet<>(set); + int count = 0; + for (Node node : subset) { + if (all.contains(node)) { + count++; + } + } + return ((double) count) / ((double) set.size()); + } - public double duration() { - return duration / 1000.0; - } + public double duration() { + return duration / 1000.0; + } - public String toString() { - return name + "\t" + duration; - } - } + public String toString() { + return name + "\t" + duration; + } + } - @SuppressWarnings("SameParameterValue") - private void runTestPointSetGeoptimaIntersection(String tracePath, String dbRoot, String dbName, String layerName, boolean testMultiPoint) { - withDatabase(dbRoot, dbName, Neo4jTestCase.NORMAL_CONFIG, graphDb -> { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - try { - int indexCount; - try (Transaction tx = graphDb.beginTx()) { - long numberOfNodes = (Long) tx.execute("MATCH (n) RETURN count(n)").columnAs("count(n)").next(); - long numberOfRelationships = (Long) tx.execute("MATCH ()-[r]->() RETURN count(r)").columnAs("count(r)").next(); - System.out.println("Opened database with " + numberOfNodes + " nodes and " + numberOfRelationships + " relationships"); + @SuppressWarnings("SameParameterValue") + private void runTestPointSetGeoptimaIntersection(String tracePath, String dbRoot, String dbName, String layerName, + boolean testMultiPoint) { + withDatabase(dbRoot, dbName, Neo4jTestCase.NORMAL_CONFIG, graphDb -> { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + try { + int indexCount; + try (Transaction tx = graphDb.beginTx()) { + long numberOfNodes = (Long) tx.execute("MATCH (n) RETURN count(n)").columnAs("count(n)").next(); + long numberOfRelationships = (Long) tx.execute("MATCH ()-[r]->() RETURN count(r)") + .columnAs("count(r)").next(); + System.out.println("Opened database with " + numberOfNodes + " nodes and " + numberOfRelationships + + " relationships"); - System.out.println("Searching for '" + layerName + "' in " + spatial.getLayerNames(tx).length + " layers:"); - for (String name : spatial.getLayerNames(tx)) { - System.out.println("\t" + name); - } - //OSMLayer layer = (OSMLayer) spatial.getOrCreateLayer(layerName, OSMGeometryEncoder.class, OSMLayer.class); - Layer layer = spatial.getLayer(tx, layerName); - assertNotNull(layer.getIndex(), "Layer index should not be null"); - assertNotNull(layer.getIndex().getBoundingBox(tx), "Layer index envelope should not be null"); - Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); - TestOSMImport.debugEnvelope(bbox, layerName, Constants.PROP_BBOX); - indexCount = TestOSMImport.checkIndexCount(tx, layer); - tx.commit(); - } - TestOSMImport.checkFeatureCount(graphDb, indexCount, layerName); + System.out.println( + "Searching for '" + layerName + "' in " + spatial.getLayerNames(tx).length + " layers:"); + for (String name : spatial.getLayerNames(tx)) { + System.out.println("\t" + name); + } + //OSMLayer layer = (OSMLayer) spatial.getOrCreateLayer(layerName, OSMGeometryEncoder.class, OSMLayer.class); + Layer layer = spatial.getLayer(tx, layerName); + assertNotNull(layer.getIndex(), "Layer index should not be null"); + assertNotNull(layer.getIndex().getBoundingBox(tx), "Layer index envelope should not be null"); + Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); + TestOSMImport.debugEnvelope(bbox, layerName, Constants.PROP_BBOX); + indexCount = TestOSMImport.checkIndexCount(tx, layer); + tx.commit(); + } + TestOSMImport.checkFeatureCount(graphDb, indexCount, layerName); - HashMap performances = new LinkedHashMap<>(); + HashMap performances = new LinkedHashMap<>(); - // Import the sample data of points on a path (drive test) - ArrayList coordinates = new ArrayList<>(); - BufferedReader locations = new BufferedReader(new FileReader(tracePath)); - String line; - Performance performance = new Performance("import"); - while ((line = locations.readLine()) != null) { - String[] fields = line.split("\\s"); - if (fields.length > 1) { - double latitude = Double.parseDouble(fields[0]); - double longitude = Double.parseDouble(fields[1]); - coordinates.add(new Coordinate(longitude, latitude)); - } - } - locations.close(); - performance.stop(coordinates.size()); - performances.put(performance.name, performance); + // Import the sample data of points on a path (drive test) + ArrayList coordinates = new ArrayList<>(); + BufferedReader locations = new BufferedReader(new FileReader(tracePath)); + String line; + Performance performance = new Performance("import"); + while ((line = locations.readLine()) != null) { + String[] fields = line.split("\\s"); + if (fields.length > 1) { + double latitude = Double.parseDouble(fields[0]); + double longitude = Double.parseDouble(fields[1]); + coordinates.add(new Coordinate(longitude, latitude)); + } + } + locations.close(); + performance.stop(coordinates.size()); + performances.put(performance.name, performance); - // Slow Test, iterating over all Point objects - double distanceInKm = 0.01; - HashSet results = new HashSet<>(); - System.out.println("Searching for geometries near " + coordinates.size() + " locations: " + coordinates.get(0) + " ... " - + coordinates.get(coordinates.size() - 1)); - performance = new Performance("search points"); - try (Transaction tx = graphDb.beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - for (Coordinate coordinate : coordinates) { - List res = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, coordinate, distanceInKm) - .sort(OrthodromicDistance.DISTANCE).toNodeList(); - results.addAll(res); - } - printResults(results); - performance.stop(results); - tx.commit(); - } - performances.put(performance.name, performance); - System.out.println("Point search took " + performance.duration() + " seconds to find " + results.size() + " results"); + // Slow Test, iterating over all Point objects + double distanceInKm = 0.01; + HashSet results = new HashSet<>(); + System.out.println( + "Searching for geometries near " + coordinates.size() + " locations: " + coordinates.get(0) + + " ... " + + coordinates.get(coordinates.size() - 1)); + performance = new Performance("search points"); + try (Transaction tx = graphDb.beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + for (Coordinate coordinate : coordinates) { + List res = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, coordinate, + distanceInKm) + .sort(OrthodromicDistance.DISTANCE).toNodeList(); + results.addAll(res); + } + printResults(results); + performance.stop(results); + tx.commit(); + } + performances.put(performance.name, performance); + System.out.println("Point search took " + performance.duration() + " seconds to find " + results.size() + + " results"); - // Faster tests with LineString and MultiPoint - GeometryFactory geometryFactory = new GeometryFactory(); - CoordinateArraySequence cseq = new CoordinateArraySequence(coordinates.toArray(new Coordinate[0])); - HashMap testGeoms = new LinkedHashMap<>(); - testGeoms.put("LineString", geometryFactory.createLineString(cseq)); - testGeoms.put("LineString.buffer(0.001)", testGeoms.get("LineString").buffer(0.001)); - testGeoms.put("LineString.buffer(0.0001)", testGeoms.get("LineString").buffer(0.0001)); - testGeoms.put("LineString.buffer(0.00001)", testGeoms.get("LineString").buffer(0.00001)); - testGeoms.put("Simplified.LS.buffer(0.0001)", TopologyPreservingSimplifier.simplify(testGeoms.get("LineString").buffer(0.0001), 0.00005)); - if (testMultiPoint) { - testGeoms.put("MultiPoint", geometryFactory.createMultiPoint(cseq)); - testGeoms.put("MultiPoint.buffer(0.001)", testGeoms.get("MultiPoint").buffer(0.001)); - testGeoms.put("MultiPoint.buffer(0.0001)", testGeoms.get("MultiPoint").buffer(0.0001)); - testGeoms.put("MultiPoint.buffer(0.00001)", testGeoms.get("MultiPoint").buffer(0.00001)); - testGeoms.put("Simplified.MP.buffer(0.0001)", TopologyPreservingSimplifier.simplify(testGeoms.get("MultiPoint").buffer(0.0001), 0.00005)); - } - for (Entry entry : testGeoms.entrySet()) { - String gname = entry.getKey(); - Geometry geometry = entry.getValue(); - System.out.println("Searching for geometries near Geometry: " + gname); - performance = new Performance(gname); - try (Transaction tx = graphDb.beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - List res = runSearch(GeoPipeline.startIntersectSearch(tx, layer, geometry), true); - performance.stop(res); - performances.put(performance.name, performance); - System.out.println("Geometry search took " + performance.duration() + " seconds to find " + res.size() + " results"); - tx.commit(); - } - } + // Faster tests with LineString and MultiPoint + GeometryFactory geometryFactory = new GeometryFactory(); + CoordinateArraySequence cseq = new CoordinateArraySequence(coordinates.toArray(new Coordinate[0])); + HashMap testGeoms = new LinkedHashMap<>(); + testGeoms.put("LineString", geometryFactory.createLineString(cseq)); + testGeoms.put("LineString.buffer(0.001)", testGeoms.get("LineString").buffer(0.001)); + testGeoms.put("LineString.buffer(0.0001)", testGeoms.get("LineString").buffer(0.0001)); + testGeoms.put("LineString.buffer(0.00001)", testGeoms.get("LineString").buffer(0.00001)); + testGeoms.put("Simplified.LS.buffer(0.0001)", + TopologyPreservingSimplifier.simplify(testGeoms.get("LineString").buffer(0.0001), 0.00005)); + if (testMultiPoint) { + testGeoms.put("MultiPoint", geometryFactory.createMultiPoint(cseq)); + testGeoms.put("MultiPoint.buffer(0.001)", testGeoms.get("MultiPoint").buffer(0.001)); + testGeoms.put("MultiPoint.buffer(0.0001)", testGeoms.get("MultiPoint").buffer(0.0001)); + testGeoms.put("MultiPoint.buffer(0.00001)", testGeoms.get("MultiPoint").buffer(0.00001)); + testGeoms.put("Simplified.MP.buffer(0.0001)", + TopologyPreservingSimplifier.simplify(testGeoms.get("MultiPoint").buffer(0.0001), 0.00005)); + } + for (Entry entry : testGeoms.entrySet()) { + String gname = entry.getKey(); + Geometry geometry = entry.getValue(); + System.out.println("Searching for geometries near Geometry: " + gname); + performance = new Performance(gname); + try (Transaction tx = graphDb.beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + List res = runSearch(GeoPipeline.startIntersectSearch(tx, layer, geometry), true); + performance.stop(res); + performances.put(performance.name, performance); + System.out.println( + "Geometry search took " + performance.duration() + " seconds to find " + res.size() + + " results"); + tx.commit(); + } + } - // Print summary of results - System.out.println("\nActivity\tDuration\tResults\tOverlap"); - for (Performance perf : performances.values()) { - System.out.println(perf.name + "\t" + perf.duration() + "\t" + perf.count + "\t" + perf.overlaps(results)); - } + // Print summary of results + System.out.println("\nActivity\tDuration\tResults\tOverlap"); + for (Performance perf : performances.values()) { + System.out.println( + perf.name + "\t" + perf.duration() + "\t" + perf.count + "\t" + perf.overlaps(results)); + } - // For lambda exceptions - return null; - } catch (Exception e) { - // For lambda exceptions - return e; - } - }); - } + // For lambda exceptions + return null; + } catch (Exception e) { + // For lambda exceptions + return e; + } + }); + } - private void printResults(Collection results) { - System.out.println("\tFound " + results.size() + " results:"); - int count = 0; - for (Node node : results) { - Object name = node.hasProperty("NAME") ? node.getProperty("NAME") : (node.hasProperty("name") ? node - .getProperty("name") : node); - System.out.println("\t\t" + name); - if (++count >= 5) { - break; - } - } - if (results.size() > 5) { - System.out.println("\t\t..."); - } - } + private void printResults(Collection results) { + System.out.println("\tFound " + results.size() + " results:"); + int count = 0; + for (Node node : results) { + Object name = node.hasProperty("NAME") ? node.getProperty("NAME") : (node.hasProperty("name") ? node + .getProperty("name") : node); + System.out.println("\t\t" + name); + if (++count >= 5) { + break; + } + } + if (results.size() > 5) { + System.out.println("\t\t..."); + } + } - @SuppressWarnings("SameParameterValue") - private List runSearch(GeoPipeline pipeline, boolean verbose) { - List results = pipeline.toNodeList(); - if (verbose) { - printResults(results); - } - return results; - } + @SuppressWarnings("SameParameterValue") + private List runSearch(GeoPipeline pipeline, boolean verbose) { + List results = pipeline.toNodeList(); + if (verbose) { + printResults(results); + } + return results; + } - private static void withDatabase(String dbRoot, String dbName, Map, Object> rawConfig, Function withDb) throws RuntimeException { - DatabaseManagementService databases = new DatabaseManagementServiceBuilder(new File(dbRoot, dbName).toPath()).setConfig(rawConfig).build(); - try { - GraphDatabaseService graphDb = databases.database(DEFAULT_DATABASE_NAME); - Exception e = withDb.apply(graphDb); - if (e != null) throw new RuntimeException(e); - } finally { - databases.shutdown(); - } - } + private static void withDatabase(String dbRoot, String dbName, Map, Object> rawConfig, + Function withDb) throws RuntimeException { + DatabaseManagementService databases = new DatabaseManagementServiceBuilder( + new File(dbRoot, dbName).toPath()).setConfig(rawConfig).build(); + try { + GraphDatabaseService graphDb = databases.database(DEFAULT_DATABASE_NAME); + Exception e = withDb.apply(graphDb); + if (e != null) { + throw new RuntimeException(e); + } + } finally { + databases.shutdown(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestOSMImport.java b/src/test/java/org/neo4j/gis/spatial/TestOSMImport.java index ecd6f5b09..2deade32b 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestOSMImport.java +++ b/src/test/java/org/neo4j/gis/spatial/TestOSMImport.java @@ -19,6 +19,11 @@ */ package org.neo4j.gis.spatial; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -28,104 +33,100 @@ import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.Transaction; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - public class TestOSMImport extends TestOSMImportBase { - public static final String spatialTestMode = System.getProperty("spatial.test.mode"); - private static final Stream parameters() { - deleteBaseDir(); - String[] smallModels = new String[]{"one-street.osm", "two-street.osm"}; + public static final String spatialTestMode = System.getProperty("spatial.test.mode"); + + private static final Stream parameters() { + deleteBaseDir(); + String[] smallModels = new String[]{"one-street.osm", "two-street.osm"}; // String[] mediumModels = new String[] { "map.osm", "map2.osm" }; - String[] mediumModels = new String[]{"map.osm"}; - String[] largeModels = new String[]{"cyprus.osm", "croatia.osm", "denmark.osm"}; + String[] mediumModels = new String[]{"map.osm"}; + String[] largeModels = new String[]{"cyprus.osm", "croatia.osm", "denmark.osm"}; - // Setup default test cases (short or medium only, no long cases) - // layersToTest.addAll(Arrays.asList(smallModels)); - ArrayList layersToTest = new ArrayList<>(Arrays.asList(mediumModels)); + // Setup default test cases (short or medium only, no long cases) + // layersToTest.addAll(Arrays.asList(smallModels)); + ArrayList layersToTest = new ArrayList<>(Arrays.asList(mediumModels)); - // Now modify the test cases based on the spatial.test.mode setting - if (spatialTestMode != null && spatialTestMode.equals("long")) { - // Very long running tests - layersToTest.addAll(Arrays.asList(largeModels)); - } else if (spatialTestMode != null && spatialTestMode.equals("short")) { - // Tests used for a quick check - layersToTest.clear(); - layersToTest.addAll(Arrays.asList(smallModels)); - } else if (spatialTestMode != null && spatialTestMode.equals("dev")) { - // Tests relevant to current development - layersToTest.clear(); + // Now modify the test cases based on the spatial.test.mode setting + if (spatialTestMode != null && spatialTestMode.equals("long")) { + // Very long running tests + layersToTest.addAll(Arrays.asList(largeModels)); + } else if (spatialTestMode != null && spatialTestMode.equals("short")) { + // Tests used for a quick check + layersToTest.clear(); + layersToTest.addAll(Arrays.asList(smallModels)); + } else if (spatialTestMode != null && spatialTestMode.equals("dev")) { + // Tests relevant to current development + layersToTest.clear(); // layersToTest.add("/home/craig/Desktop/AWE/Data/MapData/baden-wurttemberg.osm/baden-wurttemberg.osm"); - layersToTest.addAll(Arrays.asList(largeModels)); - } - boolean[] pointsTestModes = new boolean[]{true, false}; + layersToTest.addAll(Arrays.asList(largeModels)); + } + boolean[] pointsTestModes = new boolean[]{true, false}; - // Finally, build the set of complete test cases based on the collection above - ArrayList params = new ArrayList<>(); - for (final String layerName : layersToTest) { - for (final boolean includePoints : pointsTestModes) { - params.add(Arguments.of(layerName, includePoints)); - } - } - System.out.println("This suite has " + params.size() + " tests"); - for (Arguments arguments : params) { - System.out.println("\t" + Arrays.toString(arguments.get())); - } - return params.stream(); - } + // Finally, build the set of complete test cases based on the collection above + ArrayList params = new ArrayList<>(); + for (final String layerName : layersToTest) { + for (final boolean includePoints : pointsTestModes) { + params.add(Arguments.of(layerName, includePoints)); + } + } + System.out.println("This suite has " + params.size() + " tests"); + for (Arguments arguments : params) { + System.out.println("\t" + Arrays.toString(arguments.get())); + } + return params.stream(); + } - @ParameterizedTest - @MethodSource("parameters") - public void runTest(String layerName, boolean includePoints) throws Exception { - runImport(layerName, includePoints); - try (Transaction tx = graphDb().beginTx()) { - for (Node n : tx.getAllNodes()) { - debugNode(n); - } - tx.commit(); - } - } + @ParameterizedTest + @MethodSource("parameters") + public void runTest(String layerName, boolean includePoints) throws Exception { + runImport(layerName, includePoints); + try (Transaction tx = graphDb().beginTx()) { + for (Node n : tx.getAllNodes()) { + debugNode(n); + } + tx.commit(); + } + } - @Test - public void buildDataModel() { - String n1Id; - String n2Id; - try (Transaction tx = this.graphDb().beginTx()) { - Node n1 = tx.createNode(); - n1.setProperty("name", "n1"); - Node n2 = tx.createNode(); - n2.setProperty("name", "n2"); - n1.createRelationshipTo(n2, RelationshipType.withName("LIKES")); - n1Id = n1.getElementId(); - n2Id = n2.getElementId(); - debugNode(n1); - debugNode(n2); - tx.commit(); - } - try (Transaction tx = this.graphDb().beginTx()) { - for (Node n : tx.getAllNodes()) { - debugNode(n); - } - tx.commit(); - } - } + @Test + public void buildDataModel() { + String n1Id; + String n2Id; + try (Transaction tx = this.graphDb().beginTx()) { + Node n1 = tx.createNode(); + n1.setProperty("name", "n1"); + Node n2 = tx.createNode(); + n2.setProperty("name", "n2"); + n1.createRelationshipTo(n2, RelationshipType.withName("LIKES")); + n1Id = n1.getElementId(); + n2Id = n2.getElementId(); + debugNode(n1); + debugNode(n2); + tx.commit(); + } + try (Transaction tx = this.graphDb().beginTx()) { + for (Node n : tx.getAllNodes()) { + debugNode(n); + } + tx.commit(); + } + } - private void debugNode(Node node) { - Map properties = node.getProperties(); - System.out.println(node + " has " + properties.size() + " properties"); - for (Map.Entry property : properties.entrySet()) { - System.out.println(" key: " + property.getKey()); - System.out.println(" value: " + property.getValue()); - } - Iterable relationships = node.getRelationships(); - long count = StreamSupport.stream(relationships.spliterator(), false).count(); - System.out.println(node + " has " + count + " relationships"); - for (Relationship relationship : relationships) { - System.out.println(" (" + relationship.getStartNode() + ")-[:" + relationship.getType() + "]->(" + relationship.getEndNode() + ")"); - } - } + private void debugNode(Node node) { + Map properties = node.getProperties(); + System.out.println(node + " has " + properties.size() + " properties"); + for (Map.Entry property : properties.entrySet()) { + System.out.println(" key: " + property.getKey()); + System.out.println(" value: " + property.getValue()); + } + Iterable relationships = node.getRelationships(); + long count = StreamSupport.stream(relationships.spliterator(), false).count(); + System.out.println(node + " has " + count + " relationships"); + for (Relationship relationship : relationships) { + System.out.println(" (" + relationship.getStartNode() + ")-[:" + relationship.getType() + "]->(" + + relationship.getEndNode() + ")"); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestOSMImportBase.java b/src/test/java/org/neo4j/gis/spatial/TestOSMImportBase.java index 0b00cfd83..881de89b2 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestOSMImportBase.java +++ b/src/test/java/org/neo4j/gis/spatial/TestOSMImportBase.java @@ -19,6 +19,12 @@ */ package org.neo4j.gis.spatial; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import org.geotools.api.data.DataStore; import org.geotools.data.neo4j.Neo4jSpatialDataStore; import org.geotools.data.simple.SimpleFeatureCollection; @@ -26,253 +32,268 @@ import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.neo4j.gis.spatial.index.IndexManager; -import org.neo4j.gis.spatial.osm.*; +import org.neo4j.gis.spatial.osm.OSMDataset; import org.neo4j.gis.spatial.osm.OSMDataset.Way; +import org.neo4j.gis.spatial.osm.OSMGeometryEncoder; +import org.neo4j.gis.spatial.osm.OSMImporter; +import org.neo4j.gis.spatial.osm.OSMLayer; +import org.neo4j.gis.spatial.osm.OSMRelation; import org.neo4j.gis.spatial.pipes.osm.OSMGeoPipeline; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - public class TestOSMImportBase extends Neo4jTestCase { - protected static String checkOSMFile(String osm) { - File osmFile = new File(osm); - if (!osmFile.exists()) { - osmFile = new File(new File("osm"), osm); - if (!osmFile.exists()) { - return null; - } - } - return osmFile.getPath(); - } + protected static String checkOSMFile(String osm) { + File osmFile = new File(osm); + if (!osmFile.exists()) { + osmFile = new File(new File("osm"), osm); + if (!osmFile.exists()) { + return null; + } + } + return osmFile.getPath(); + } - protected static void checkOSMLayer(GraphDatabaseService db, String layerName) throws IOException { - int indexCount; - try (Transaction tx = db.beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); - OSMLayer layer = (OSMLayer) spatial.getOrCreateLayer(tx, layerName, OSMGeometryEncoder.class, OSMLayer.class); - Assertions.assertNotNull(layer.getIndex(), "OSM Layer index should not be null"); - Assertions.assertNotNull(layer.getIndex().getBoundingBox(tx), "OSM Layer index envelope should not be null"); - Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); - debugEnvelope(bbox, layerName, Constants.PROP_BBOX); - // ((RTreeIndex)layer.getIndex()).debugIndexTree(); - indexCount = checkIndexCount(tx, layer); - checkChangesetsAndUsers(tx, layer); - checkOSMSearch(tx, layer); - tx.commit(); - } - checkFeatureCount(db, indexCount, layerName); - } + protected static void checkOSMLayer(GraphDatabaseService db, String layerName) throws IOException { + int indexCount; + try (Transaction tx = db.beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); + OSMLayer layer = (OSMLayer) spatial.getOrCreateLayer(tx, layerName, OSMGeometryEncoder.class, + OSMLayer.class); + Assertions.assertNotNull(layer.getIndex(), "OSM Layer index should not be null"); + Assertions.assertNotNull(layer.getIndex().getBoundingBox(tx), + "OSM Layer index envelope should not be null"); + Envelope bbox = Utilities.fromNeo4jToJts(layer.getIndex().getBoundingBox(tx)); + debugEnvelope(bbox, layerName, Constants.PROP_BBOX); + // ((RTreeIndex)layer.getIndex()).debugIndexTree(); + indexCount = checkIndexCount(tx, layer); + checkChangesetsAndUsers(tx, layer); + checkOSMSearch(tx, layer); + tx.commit(); + } + checkFeatureCount(db, indexCount, layerName); + } - public static void checkOSMSearch(Transaction tx, OSMLayer layer) { - OSMDataset osm = OSMDataset.fromLayer(tx, layer); - Way way = null; - int count = 0; - for (Way wayNode : osm.getWays(tx)) { - // Do not `break` from the loop or experience the RelationshipTraversalCursor leak bug in Neo4j 4.3 - if (count++ <= 100) { - way = wayNode; - } - } - Assertions.assertNotNull(way, "Should be at least one way"); - Envelope bbox = way.getEnvelope(); - runSearches(tx, layer, bbox, true); - org.neo4j.gis.spatial.rtree.Envelope layerBBox = layer.getIndex().getBoundingBox(tx); - double[] centre = layerBBox.centre(); - double width = layerBBox.getWidth(0) / 100.0; - double height = layerBBox.getWidth(1) / 100.0; - bbox = new Envelope(centre[0] - width, centre[0] + width, centre[1] - height, centre[1] + height); - runSearches(tx, layer, bbox, false); - } + public static void checkOSMSearch(Transaction tx, OSMLayer layer) { + OSMDataset osm = OSMDataset.fromLayer(tx, layer); + Way way = null; + int count = 0; + for (Way wayNode : osm.getWays(tx)) { + // Do not `break` from the loop or experience the RelationshipTraversalCursor leak bug in Neo4j 4.3 + if (count++ <= 100) { + way = wayNode; + } + } + Assertions.assertNotNull(way, "Should be at least one way"); + Envelope bbox = way.getEnvelope(); + runSearches(tx, layer, bbox, true); + org.neo4j.gis.spatial.rtree.Envelope layerBBox = layer.getIndex().getBoundingBox(tx); + double[] centre = layerBBox.centre(); + double width = layerBBox.getWidth(0) / 100.0; + double height = layerBBox.getWidth(1) / 100.0; + bbox = new Envelope(centre[0] - width, centre[0] + width, centre[1] - height, centre[1] + height); + runSearches(tx, layer, bbox, false); + } - private static void runSearches(Transaction tx, OSMLayer layer, Envelope bbox, boolean willHaveResult) { - for (int i = 0; i < 4; i++) { - Geometry searchArea = layer.getGeometryFactory().toGeometry(bbox); - runWithinSearch(tx, layer, searchArea, willHaveResult); - bbox.expandBy(bbox.getWidth(), bbox.getHeight()); - } - } + private static void runSearches(Transaction tx, OSMLayer layer, Envelope bbox, boolean willHaveResult) { + for (int i = 0; i < 4; i++) { + Geometry searchArea = layer.getGeometryFactory().toGeometry(bbox); + runWithinSearch(tx, layer, searchArea, willHaveResult); + bbox.expandBy(bbox.getWidth(), bbox.getHeight()); + } + } - private static void runWithinSearch(Transaction tx, OSMLayer layer, Geometry searchArea, boolean willHaveResult) { - long start = System.currentTimeMillis(); - List results = OSMGeoPipeline.startWithinSearch(tx, layer, searchArea).toSpatialDatabaseRecordList(); - long time = System.currentTimeMillis() - start; - System.out.println("Took " + time + "ms to find " + results.size() + " search results in layer " + layer.getName() - + " using search within " + searchArea); - if (willHaveResult) - Assertions.assertTrue(results.size() > 0, "Should be at least one result, but got zero"); - } + private static void runWithinSearch(Transaction tx, OSMLayer layer, Geometry searchArea, boolean willHaveResult) { + long start = System.currentTimeMillis(); + List results = OSMGeoPipeline.startWithinSearch(tx, layer, searchArea) + .toSpatialDatabaseRecordList(); + long time = System.currentTimeMillis() - start; + System.out.println( + "Took " + time + "ms to find " + results.size() + " search results in layer " + layer.getName() + + " using search within " + searchArea); + if (willHaveResult) { + Assertions.assertTrue(results.size() > 0, "Should be at least one result, but got zero"); + } + } - public static void debugEnvelope(Envelope bbox, String layer, String name) { - System.out.println("Layer '" + layer + "' has envelope '" + name + "': " + bbox); - System.out.println("\tX: [" + bbox.getMinX() + ":" + bbox.getMaxX() + "]"); - System.out.println("\tY: [" + bbox.getMinY() + ":" + bbox.getMaxY() + "]"); - } + public static void debugEnvelope(Envelope bbox, String layer, String name) { + System.out.println("Layer '" + layer + "' has envelope '" + name + "': " + bbox); + System.out.println("\tX: [" + bbox.getMinX() + ":" + bbox.getMaxX() + "]"); + System.out.println("\tY: [" + bbox.getMinY() + ":" + bbox.getMaxY() + "]"); + } - public static int checkIndexCount(Transaction tx, Layer layer) { - if (layer.getIndex().count(tx) < 1) { - System.out.println("Warning: index count zero: " + layer.getName()); - } - System.out.println("Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); - return layer.getIndex().count(tx); - } + public static int checkIndexCount(Transaction tx, Layer layer) { + if (layer.getIndex().count(tx) < 1) { + System.out.println("Warning: index count zero: " + layer.getName()); + } + System.out.println( + "Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); + return layer.getIndex().count(tx); + } - public static void checkFeatureCount(GraphDatabaseService db, int indexCount, String layerName) throws IOException { - DataStore store = new Neo4jSpatialDataStore(db); - SimpleFeatureCollection features = store.getFeatureSource(layerName).getFeatures(); - int featuresSize = features.size(); - System.out.println("Layer '" + layerName + "' has " + featuresSize + " features"); - Assertions.assertEquals(indexCount, featuresSize, "FeatureCollection.size for layer '" + layerName + "' not the same as index count"); - } + public static void checkFeatureCount(GraphDatabaseService db, int indexCount, String layerName) throws IOException { + DataStore store = new Neo4jSpatialDataStore(db); + SimpleFeatureCollection features = store.getFeatureSource(layerName).getFeatures(); + int featuresSize = features.size(); + System.out.println("Layer '" + layerName + "' has " + featuresSize + " features"); + Assertions.assertEquals(indexCount, featuresSize, + "FeatureCollection.size for layer '" + layerName + "' not the same as index count"); + } - private static void checkChangesetsAndUsers(Transaction tx, OSMLayer layer) { - double totalMatch = 0.0; - int waysMatched = 0; - int waysCounted = 0; - int nodesCounted = 0; - int waysMissing = 0; - int nodesMissing = 0; - int usersMissing = 0; - float maxMatch = 0.0f; - float minMatch = 1.0f; - HashMap userNodeCount = new HashMap<>(); - HashMap userNames = new HashMap<>(); - HashMap userIds = new HashMap<>(); - OSMDataset dataset = OSMDataset.fromLayer(tx, layer); - for (Node way : dataset.getAllWayNodes(tx)) { - int node_count = 0; - int match_count = 0; - Assertions.assertNull(way.getProperty("changeset", null), "Way has changeset property"); - Node wayChangeset = dataset.getChangeset(way); - if (wayChangeset != null) { - long wayCS = (Long) wayChangeset.getProperty("changeset"); - for (Node node : dataset.getWayNodes(way)) { - Assertions.assertNull(node.getProperty("changeset", null), "Node has changeset property"); - Node nodeChangeset = dataset.getChangeset(node); - if (nodeChangeset == null) { - nodesMissing++; - } else { - long nodeCS = (Long) nodeChangeset.getProperty("changeset"); - if (nodeChangeset.equals(wayChangeset)) { - match_count++; - } else { - Assertions.assertNotEquals(wayCS, nodeCS, "Two changeset nodes should not have the same changeset number: way(" + wayCS + ")==node(" + nodeCS + ")"); - } - Node user = dataset.getUser(nodeChangeset); - if (user != null) { - String userid = user.getElementId(); - if (userNodeCount.containsKey(userid)) { - userNodeCount.put(userid, userNodeCount.get(userid) + 1); - } else { - userNodeCount.put(userid, 1); - userNames.put(userid, (String) user.getProperty("name", null)); - userIds.put(userid, (Long) user.getProperty("uid", null)); - } - } else { - if (usersMissing++ < 10) { - System.out.println("Changeset " + nodeCS + " should have user: " + nodeChangeset); - } - } - } - node_count++; - } - } else { - waysMissing++; - } - if (node_count > 0) { - waysMatched++; - float match = ((float) match_count) / ((float) node_count); - maxMatch = Math.max(maxMatch, match); - minMatch = Math.min(minMatch, match); - totalMatch += match; - nodesCounted += node_count; - } - waysCounted++; - } - System.out.println("After checking " + waysCounted + " ways:"); - System.out.println("\twe found " + waysMatched + " ways with an average of " + (nodesCounted / waysMatched) + " nodes"); - System.out.println("\tand an average of " + (100.0 * totalMatch / waysMatched) + "% matching changesets"); - System.out.println("\twith min-match " + (100.0 * minMatch) + "% and max-match " + (100.0 * maxMatch) + "%"); - System.out.println("\tWays missing changsets: " + waysMissing); - System.out.println("\tNodes missing changsets: " + nodesMissing + " (~" + (nodesMissing / waysMatched) + " / way)"); - System.out.println("\tUnique users: " + userNodeCount.size() + " (with " + usersMissing + " changeset missing users)"); - ArrayList> userCounts = new ArrayList<>(); - for (String user : userNodeCount.keySet()) { - int count = userNodeCount.get(user); - userCounts.ensureCapacity(count); - while (userCounts.size() < count + 1) { - userCounts.add(null); - } - ArrayList userSet = userCounts.get(count); - if (userSet == null) { - userSet = new ArrayList<>(); - } - userSet.add(user); - userCounts.set(count, userSet); - } - if (userCounts.size() > 1) { - System.out.println("\tTop 20 users (nodes: users):"); - for (int ui = userCounts.size() - 1, i = 0; i < 20 && ui >= 0; ui--) { - ArrayList userSet = userCounts.get(ui); - if (userSet != null && userSet.size() > 0) { - i++; - StringBuilder us = new StringBuilder(); - for (String user : userSet) { - Node userNode = tx.getNodeByElementId(user); - int csCount = 0; - for (@SuppressWarnings("unused") - Relationship rel : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { - csCount++; - } - String name = userNames.get(user); - Long uid = userIds.get(user); - if (us.length() > 0) { - us.append(", "); - } - us.append(String.format("%s (uid=%d, id=%s, changesets=%d)", name, uid, user, csCount)); - } - System.out.println("\t\t" + ui + ": " + us); - } - } - } - } + private static void checkChangesetsAndUsers(Transaction tx, OSMLayer layer) { + double totalMatch = 0.0; + int waysMatched = 0; + int waysCounted = 0; + int nodesCounted = 0; + int waysMissing = 0; + int nodesMissing = 0; + int usersMissing = 0; + float maxMatch = 0.0f; + float minMatch = 1.0f; + HashMap userNodeCount = new HashMap<>(); + HashMap userNames = new HashMap<>(); + HashMap userIds = new HashMap<>(); + OSMDataset dataset = OSMDataset.fromLayer(tx, layer); + for (Node way : dataset.getAllWayNodes(tx)) { + int node_count = 0; + int match_count = 0; + Assertions.assertNull(way.getProperty("changeset", null), "Way has changeset property"); + Node wayChangeset = dataset.getChangeset(way); + if (wayChangeset != null) { + long wayCS = (Long) wayChangeset.getProperty("changeset"); + for (Node node : dataset.getWayNodes(way)) { + Assertions.assertNull(node.getProperty("changeset", null), "Node has changeset property"); + Node nodeChangeset = dataset.getChangeset(node); + if (nodeChangeset == null) { + nodesMissing++; + } else { + long nodeCS = (Long) nodeChangeset.getProperty("changeset"); + if (nodeChangeset.equals(wayChangeset)) { + match_count++; + } else { + Assertions.assertNotEquals(wayCS, nodeCS, + "Two changeset nodes should not have the same changeset number: way(" + wayCS + + ")==node(" + nodeCS + ")"); + } + Node user = dataset.getUser(nodeChangeset); + if (user != null) { + String userid = user.getElementId(); + if (userNodeCount.containsKey(userid)) { + userNodeCount.put(userid, userNodeCount.get(userid) + 1); + } else { + userNodeCount.put(userid, 1); + userNames.put(userid, (String) user.getProperty("name", null)); + userIds.put(userid, (Long) user.getProperty("uid", null)); + } + } else { + if (usersMissing++ < 10) { + System.out.println("Changeset " + nodeCS + " should have user: " + nodeChangeset); + } + } + } + node_count++; + } + } else { + waysMissing++; + } + if (node_count > 0) { + waysMatched++; + float match = ((float) match_count) / ((float) node_count); + maxMatch = Math.max(maxMatch, match); + minMatch = Math.min(minMatch, match); + totalMatch += match; + nodesCounted += node_count; + } + waysCounted++; + } + System.out.println("After checking " + waysCounted + " ways:"); + System.out.println( + "\twe found " + waysMatched + " ways with an average of " + (nodesCounted / waysMatched) + " nodes"); + System.out.println("\tand an average of " + (100.0 * totalMatch / waysMatched) + "% matching changesets"); + System.out.println("\twith min-match " + (100.0 * minMatch) + "% and max-match " + (100.0 * maxMatch) + "%"); + System.out.println("\tWays missing changsets: " + waysMissing); + System.out.println( + "\tNodes missing changsets: " + nodesMissing + " (~" + (nodesMissing / waysMatched) + " / way)"); + System.out.println( + "\tUnique users: " + userNodeCount.size() + " (with " + usersMissing + " changeset missing users)"); + ArrayList> userCounts = new ArrayList<>(); + for (String user : userNodeCount.keySet()) { + int count = userNodeCount.get(user); + userCounts.ensureCapacity(count); + while (userCounts.size() < count + 1) { + userCounts.add(null); + } + ArrayList userSet = userCounts.get(count); + if (userSet == null) { + userSet = new ArrayList<>(); + } + userSet.add(user); + userCounts.set(count, userSet); + } + if (userCounts.size() > 1) { + System.out.println("\tTop 20 users (nodes: users):"); + for (int ui = userCounts.size() - 1, i = 0; i < 20 && ui >= 0; ui--) { + ArrayList userSet = userCounts.get(ui); + if (userSet != null && userSet.size() > 0) { + i++; + StringBuilder us = new StringBuilder(); + for (String user : userSet) { + Node userNode = tx.getNodeByElementId(user); + int csCount = 0; + for (@SuppressWarnings("unused") + Relationship rel : userNode.getRelationships(Direction.INCOMING, OSMRelation.USER)) { + csCount++; + } + String name = userNames.get(user); + Long uid = userIds.get(user); + if (us.length() > 0) { + us.append(", "); + } + us.append(String.format("%s (uid=%d, id=%s, changesets=%d)", name, uid, user, csCount)); + } + System.out.println("\t\t" + ui + ": " + us); + } + } + } + } - protected void runImport(String osm, boolean includePoints) throws Exception { - // TODO: Consider merits of using dependency data in target/osm, - // downloaded by maven, as done in TestSpatial, versus the test data - // committed to source code as done here - String osmPath = checkOSMFile(osm); - if (osmPath == null) { - return; - } - printDatabaseStats(); - loadTestOsmData(osm, osmPath, includePoints); - checkOSMLayer(graphDb(), osm); - printDatabaseStats(); - } + protected void runImport(String osm, boolean includePoints) throws Exception { + // TODO: Consider merits of using dependency data in target/osm, + // downloaded by maven, as done in TestSpatial, versus the test data + // committed to source code as done here + String osmPath = checkOSMFile(osm); + if (osmPath == null) { + return; + } + printDatabaseStats(); + loadTestOsmData(osm, osmPath, includePoints); + checkOSMLayer(graphDb(), osm); + printDatabaseStats(); + } - protected void loadTestOsmData(String layerName, String osmPath, boolean includePoints) throws Exception { - System.out.printf("\n=== Loading layer '%s' from %s, includePoints=%b ===\n", layerName, osmPath, includePoints); - long start = System.currentTimeMillis(); - // tag::importOsm[] START SNIPPET: importOsm - OSMImporter importer = new OSMImporter(layerName, new ConsoleListener()); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(graphDb(), osmPath, includePoints, 5000); - // end::importOsm[] END SNIPPET: importOsm - // Weird hack to force GC on large loads - if (System.currentTimeMillis() - start > 300000) { - for (int i = 0; i < 3; i++) { - System.gc(); - Thread.sleep(1000); - } - } - importer.reIndex(graphDb(), 1000, includePoints); - } + protected void loadTestOsmData(String layerName, String osmPath, boolean includePoints) throws Exception { + System.out.printf("\n=== Loading layer '%s' from %s, includePoints=%b ===\n", layerName, osmPath, + includePoints); + long start = System.currentTimeMillis(); + // tag::importOsm[] START SNIPPET: importOsm + OSMImporter importer = new OSMImporter(layerName, new ConsoleListener()); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(graphDb(), osmPath, includePoints, 5000); + // end::importOsm[] END SNIPPET: importOsm + // Weird hack to force GC on large loads + if (System.currentTimeMillis() - start > 300000) { + for (int i = 0; i < 3; i++) { + System.gc(); + Thread.sleep(1000); + } + } + importer.reIndex(graphDb(), 1000, includePoints); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestProcess.java b/src/test/java/org/neo4j/gis/spatial/TestProcess.java index 2fce88584..b391d3b00 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestProcess.java +++ b/src/test/java/org/neo4j/gis/spatial/TestProcess.java @@ -19,50 +19,49 @@ */ package org.neo4j.gis.spatial; -import org.junit.jupiter.api.Test; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; +import java.util.Map; +import java.util.concurrent.ExecutionException; import org.geotools.api.data.Parameter; +import org.geotools.api.feature.type.Name; import org.geotools.feature.NameImpl; import org.geotools.process.ProcessExecutor; import org.geotools.process.Processors; import org.geotools.process.Progress; import org.geotools.util.KVP; -import org.geotools.api.feature.type.Name; - -import java.util.Map; -import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; public class TestProcess extends Neo4jTestCase { - @Test - public void testProcess() throws ParseException, InterruptedException, ExecutionException { - WKTReader wktReader = new WKTReader(new GeometryFactory()); - Geometry geom = wktReader.read("MULTIPOINT (1 1, 5 4, 7 9, 5 5, 2 2)"); + @Test + public void testProcess() throws ParseException, InterruptedException, ExecutionException { + WKTReader wktReader = new WKTReader(new GeometryFactory()); + Geometry geom = wktReader.read("MULTIPOINT (1 1, 5 4, 7 9, 5 5, 2 2)"); - Name name = new NameImpl("spatial", "octagonalEnvelope"); - org.geotools.process.Process process = Processors.createProcess(name); - System.out.println("Executing process: " + name); - for (Map.Entry> entry : Processors.getParameterInfo(name).entrySet()) { - System.out.println("\t" + entry.getKey() + ":\t" + entry.getValue()); - } + Name name = new NameImpl("spatial", "octagonalEnvelope"); + org.geotools.process.Process process = Processors.createProcess(name); + System.out.println("Executing process: " + name); + for (Map.Entry> entry : Processors.getParameterInfo(name).entrySet()) { + System.out.println("\t" + entry.getKey() + ":\t" + entry.getValue()); + } - ProcessExecutor engine = Processors.newProcessExecutor(2); + ProcessExecutor engine = Processors.newProcessExecutor(2); - // quick map of inputs - Map input = new KVP("geom", geom); - Progress working = engine.submit(process, input); + // quick map of inputs + Map input = new KVP("geom", geom); + Progress working = engine.submit(process, input); - // you could do other stuff whle working is doing its thing - if (working.isCancelled()) { - return; - } + // you could do other stuff whle working is doing its thing + if (working.isCancelled()) { + return; + } - Map result = working.get(); // get is BLOCKING - Geometry octo = (Geometry) result.get("result"); + Map result = working.get(); // get is BLOCKING + Geometry octo = (Geometry) result.get("result"); - System.out.println(octo); - } + System.out.println(octo); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestReadOnlyTransactions.java b/src/test/java/org/neo4j/gis/spatial/TestReadOnlyTransactions.java index 923df286b..a72e1ee56 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestReadOnlyTransactions.java +++ b/src/test/java/org/neo4j/gis/spatial/TestReadOnlyTransactions.java @@ -1,20 +1,22 @@ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.neo4j.dbms.api.DatabaseManagementService; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.graphdb.Transaction; import org.neo4j.io.fs.FileUtils; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; - -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; -import static org.junit.jupiter.api.Assertions.*; - /** * This test was written to test the subtle behavior of nested transactions in the Neo4j 1.x-3.x code. * However, in Neo4j 4.0 transaction nesting was removed, and it is no longer possible to create inner transactions. @@ -24,103 +26,106 @@ * // TODO: Consider deleting this test as it probably no longer makes sense in Neo4j 4.0 */ public class TestReadOnlyTransactions { - private DatabaseManagementService databases; - private GraphDatabaseService graph; - private static final Path basePath = new File("target/var").toPath(); - private static final String dbPrefix = "neo4j-db"; - - private static long storePrefix; - - private static String n1Id; - private static String n2Id; - - @BeforeEach - public void setUp() throws Exception { - storePrefix++; - System.out.println("Creating store at " + dbPrefix + storePrefix); - this.databases = new TestDatabaseManagementServiceBuilder(basePath.resolve(dbPrefix + storePrefix)).impermanent().build(); - this.graph = databases.database(DEFAULT_DATABASE_NAME); - buildDataModel(); - } - - @AfterEach - public void tearDown() { - databases.shutdown(); - try { - FileUtils.deleteDirectory(basePath); - } catch (IOException e) { - System.out.println("Failed to delete database: " + e); - e.printStackTrace(); - } - } - - private void buildDataModel() { - try (Transaction tx = graph.beginTx()) { - Node n1 = tx.createNode(); - n1.setProperty("name", "n1"); - Node n2 = tx.createNode(); - n2.setProperty("name", "n2"); - n1.createRelationshipTo(n2, RelationshipType.withName("LIKES")); - n1Id = n1.getElementId(); - n2Id = n2.getElementId(); - tx.commit(); - } - } - - private void readNames(Transaction tx) { - Node n1 = tx.getNodeByElementId(n1Id); - Node n2 = tx.getNodeByElementId(n2Id); - String n1Name = (String) n1.getProperty("name"); - String n2Name = (String) n2.getProperty("name"); - System.out.println("First node: " + n1Name); - System.out.println("Second node: " + n2Name); - assertEquals("n1", n1Name, "Name does not match"); - assertEquals("n2", n2Name, "Name does not match"); - } - - private void readNamesWithNestedTransaction(boolean outer, boolean inner) { - try (Transaction tx_outer = graph.beginTx()) { - try (Transaction tx_inner = graph.beginTx()) { - readNames(tx_inner); - if (inner) { - tx_inner.commit(); - } - } - if (outer) { - tx_outer.commit(); - } - } - } - - @Test - public void testNormalTransaction() { - try (Transaction tx = graph.beginTx()) { - readNames(tx); - tx.commit(); - } - } - - @Test - public void testNestedTransactionFF() { - readNamesWithNestedTransaction(false, false); - } - - @Test - public void testNestedTransactionSS() { - readNamesWithNestedTransaction(true, true); - } - - @Test - public void testNestedTransactionFS() { - readNamesWithNestedTransaction(false, true); - } - - @Test - public void testNestedTransactionSF() { - try { - readNamesWithNestedTransaction(true, false); - } catch (Exception e) { - assertEquals("Transaction rolled back even if marked as successful", e.getCause().getMessage(), "Expected transaction failure from RollbackException"); - } - } + + private DatabaseManagementService databases; + private GraphDatabaseService graph; + private static final Path basePath = new File("target/var").toPath(); + private static final String dbPrefix = "neo4j-db"; + + private static long storePrefix; + + private static String n1Id; + private static String n2Id; + + @BeforeEach + public void setUp() throws Exception { + storePrefix++; + System.out.println("Creating store at " + dbPrefix + storePrefix); + this.databases = new TestDatabaseManagementServiceBuilder( + basePath.resolve(dbPrefix + storePrefix)).impermanent().build(); + this.graph = databases.database(DEFAULT_DATABASE_NAME); + buildDataModel(); + } + + @AfterEach + public void tearDown() { + databases.shutdown(); + try { + FileUtils.deleteDirectory(basePath); + } catch (IOException e) { + System.out.println("Failed to delete database: " + e); + e.printStackTrace(); + } + } + + private void buildDataModel() { + try (Transaction tx = graph.beginTx()) { + Node n1 = tx.createNode(); + n1.setProperty("name", "n1"); + Node n2 = tx.createNode(); + n2.setProperty("name", "n2"); + n1.createRelationshipTo(n2, RelationshipType.withName("LIKES")); + n1Id = n1.getElementId(); + n2Id = n2.getElementId(); + tx.commit(); + } + } + + private void readNames(Transaction tx) { + Node n1 = tx.getNodeByElementId(n1Id); + Node n2 = tx.getNodeByElementId(n2Id); + String n1Name = (String) n1.getProperty("name"); + String n2Name = (String) n2.getProperty("name"); + System.out.println("First node: " + n1Name); + System.out.println("Second node: " + n2Name); + assertEquals("n1", n1Name, "Name does not match"); + assertEquals("n2", n2Name, "Name does not match"); + } + + private void readNamesWithNestedTransaction(boolean outer, boolean inner) { + try (Transaction tx_outer = graph.beginTx()) { + try (Transaction tx_inner = graph.beginTx()) { + readNames(tx_inner); + if (inner) { + tx_inner.commit(); + } + } + if (outer) { + tx_outer.commit(); + } + } + } + + @Test + public void testNormalTransaction() { + try (Transaction tx = graph.beginTx()) { + readNames(tx); + tx.commit(); + } + } + + @Test + public void testNestedTransactionFF() { + readNamesWithNestedTransaction(false, false); + } + + @Test + public void testNestedTransactionSS() { + readNamesWithNestedTransaction(true, true); + } + + @Test + public void testNestedTransactionFS() { + readNamesWithNestedTransaction(false, true); + } + + @Test + public void testNestedTransactionSF() { + try { + readNamesWithNestedTransaction(true, false); + } catch (Exception e) { + assertEquals("Transaction rolled back even if marked as successful", e.getCause().getMessage(), + "Expected transaction failure from RollbackException"); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestRemove.java b/src/test/java/org/neo4j/gis/spatial/TestRemove.java index bfec91025..99b6196ca 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestRemove.java +++ b/src/test/java/org/neo4j/gis/spatial/TestRemove.java @@ -28,40 +28,42 @@ import org.neo4j.kernel.internal.GraphDatabaseAPI; public class TestRemove extends Neo4jTestCase { - private static final String layerName = "TestRemove"; - @Test - public void testAddMoreThanMaxNodeRefThenDeleteAll() { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + private static final String layerName = "TestRemove"; - try (Transaction tx = graphDb().beginTx()) { - spatial.createLayer(tx, layerName, WKTGeometryEncoder.class, EditableLayerImpl.class); - tx.commit(); - } + @Test + public void testAddMoreThanMaxNodeRefThenDeleteAll() { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - int rtreeMaxNodeReferences = 100; + try (Transaction tx = graphDb().beginTx()) { + spatial.createLayer(tx, layerName, WKTGeometryEncoder.class, EditableLayerImpl.class); + tx.commit(); + } - String[] ids = new String[rtreeMaxNodeReferences + 1]; + int rtreeMaxNodeReferences = 100; - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - GeometryFactory geomFactory = layer.getGeometryFactory(); - for (int i = 0; i < ids.length; i++) { - ids[i] = layer.add(tx, geomFactory.createPoint(new Coordinate(i, i))).getNodeId(); - } - tx.commit(); - } + String[] ids = new String[rtreeMaxNodeReferences + 1]; - Neo4jTestUtils.debugIndexTree(graphDb(), layerName); + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + GeometryFactory geomFactory = layer.getGeometryFactory(); + for (int i = 0; i < ids.length; i++) { + ids[i] = layer.add(tx, geomFactory.createPoint(new Coordinate(i, i))).getNodeId(); + } + tx.commit(); + } - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - for (String id : ids) { - layer.delete(tx, id); - } - tx.commit(); - } + Neo4jTestUtils.debugIndexTree(graphDb(), layerName); - Neo4jTestUtils.debugIndexTree(graphDb(), layerName); - } -} \ No newline at end of file + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + for (String id : ids) { + layer.delete(tx, id); + } + tx.commit(); + } + + Neo4jTestUtils.debugIndexTree(graphDb(), layerName); + } +} diff --git a/src/test/java/org/neo4j/gis/spatial/TestSimplePointLayer.java b/src/test/java/org/neo4j/gis/spatial/TestSimplePointLayer.java index cb25f447d..365e26266 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestSimplePointLayer.java +++ b/src/test/java/org/neo4j/gis/spatial/TestSimplePointLayer.java @@ -19,6 +19,16 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; import junit.framework.AssertionFailedError; import org.geotools.data.neo4j.StyledImageExporter; import org.junit.jupiter.api.Test; @@ -39,402 +49,418 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.*; - public class TestSimplePointLayer extends Neo4jTestCase { - private static final Coordinate testOrigin = new Coordinate(13.0, 55.6); - - @Test - public void testNearestNeighborSearchOnEmptyLayer() { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = spatial.createSimplePointLayer(tx, layerName, "Longitude", "Latitude"); - assertNotNull(layer); - tx.commit(); - } - - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - // finds geometries around point - List results = GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(15.3, 56.2), 1.0) - .toSpatialDatabaseRecordList(); - - // should find no results - assertEquals(0, results.size()); - tx.commit(); - } - - } - - @Test - public void testSimplePointLayer() { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = spatial.createSimplePointLayer(tx, layerName, "Longitude", "Latitude"); - assertNotNull(layer); - SpatialRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); - assertNotNull(record); - tx.commit(); - } - - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - // finds geometries that contain the given geometry - Geometry geometry = layer.getGeometryFactory().toGeometry(new org.locationtech.jts.geom.Envelope(15.0, 16.0, 56.0, 57.0)); - List results = GeoPipeline.startContainSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); - - // should not be contained - assertEquals(0, results.size()); - - results = GeoPipeline.startWithinSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); - - assertEquals(1, results.size()); - tx.commit(); - } - } - - @Test - public void testNativePointLayer() { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer layer = spatial.createNativePointLayer(tx, layerName, "location"); - assertNotNull(layer); - SpatialRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); - assertNotNull(record); - }); - - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); - // finds geometries that contain the given geometry - Geometry geometry = layer.getGeometryFactory().toGeometry(new org.locationtech.jts.geom.Envelope(15.0, 16.0, 56.0, 57.0)); - List results = GeoPipeline.startContainSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); - - // should not be contained - assertEquals(0, results.size()); - - results = GeoPipeline.startWithinSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); - - assertEquals(1, results.size()); - tx.commit(); - } - } - - @Test - public void testNeoTextLayer() { - String layerName = "neo-text"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - SimplePointLayer layer = spatial.createSimplePointLayer(tx, layerName); - assertNotNull(layer); - assertNotNull(layer.getName(), "layer name is not null"); - for (Coordinate coordinate : makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin)) { - SpatialRecord record = layer.add(tx, coordinate); - assertNotNull(record); - } - }); - saveLayerAsImage(layerName, 700, 70); - - try (Transaction tx = graphDb().beginTx()) { - SimplePointLayer layer = (SimplePointLayer) spatial.getLayer(tx, layerName); - Envelope bbox = layer.getIndex().getBoundingBox(tx); - double[] centre = bbox.centre(); - - List results = GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0] + 0.1, centre[1]), 10.0) - .sort(OrthodromicDistance.DISTANCE).toList(); - - saveResultsAsImage(results, "temporary-results-layer-" + layer.getName(), 130, 70); - assertEquals(71, results.size()); - checkPointOrder(results); - - results = GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0] + 0.1, centre[1]), 5.0) - .sort(OrthodromicDistance.DISTANCE).toList(); - - saveResultsAsImage(results, "temporary-results-layer2-" + layer.getName(), 130, 70); - assertEquals(30, results.size()); - checkPointOrder(results); - - // Now test the old API - results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 10.0); - assertEquals(71, results.size()); - checkPointOrder(results); - results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 1000); - assertEquals(265, results.size()); // There are only 265 points in dataset - checkPointOrder(results); - results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 100); - assertEquals(100, results.size()); // We expect an exact count from the layer method (but not from the pipeline) - checkPointOrder(results); - results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1])); - assertEquals(100, results.size()); // The default in SimplePointLayer is 100 results - checkPointOrder(results); - tx.commit(); - } - - } - - @Test - public void testIndexingExistingSimplePointNodes() { - String layerName = "my-simple-points"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> spatial.createSimplePointLayer(tx, layerName, "x", "y")); - - Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); - inTx(tx -> { - Layer layer = spatial.getLayer(tx, layerName); - for (Coordinate coordinate : coords) { - Node n = tx.createNode(); - n.setProperty("x", coordinate.x); - n.setProperty("y", coordinate.y); - layer.add(tx, n); - } - }); - saveLayerAsImage(layerName, 700, 70); - assertIndexCountSameAs(layerName, coords.length); - } - - @Test - public void testIndexingExistingNativePointNodes() { - String layerName = "my-native-points"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> spatial.createNativePointLayer(tx, "my-native-points", "position")); - Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); - - Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); - inTx(tx -> { - Layer layer = spatial.getLayer(tx, layerName); - for (Coordinate coordinate : coords) { - Node n = tx.createNode(); - n.setProperty("x", coordinate.x); - n.setProperty("y", coordinate.y); - n.setProperty("position", new Neo4jPoint(coordinate, crs)); - layer.add(tx, n); - } - }); - saveLayerAsImage(layerName, 700, 70); - assertIndexCountSameAs(layerName, coords.length); - } - - @Test - public void testIndexingExistingPointNodesWithMultipleLocations() { - String layerNameA = "my-points-A"; - String layerNameB = "my-points-B"; - String layerNameC = "my-points-C"; - GraphDatabaseService db = graphDb(); - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - double x_offset = 0.15, y_offset = 0.15; - inTx(tx -> { - spatial.createSimplePointLayer(tx, layerNameA, "xa", "ya", "bbox_a"); - spatial.createSimplePointLayer(tx, layerNameB, "xb", "yb", "bbox_b"); - spatial.createNativePointLayer(tx, layerNameC, "loc", "bbox_c"); - }); - Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); - - Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); - try (Transaction tx = db.beginTx()) { - Layer layerA = spatial.getLayer(tx, layerNameA); - Layer layerB = spatial.getLayer(tx, layerNameB); - Layer layerC = spatial.getLayer(tx, layerNameC); - for (Coordinate coordinate : coords) { - Node n = tx.createNode(); - n.setProperty("xa", coordinate.x); - n.setProperty("ya", coordinate.y); - n.setProperty("xb", coordinate.x + x_offset); - n.setProperty("yb", coordinate.y + y_offset); - n.setProperty("loc", new Neo4jPoint(new double[]{coordinate.x + 2 * x_offset, coordinate.y + 2 * y_offset}, crs)); - - layerA.add(tx, n); - layerB.add(tx, n); - layerC.add(tx, n); - } - tx.commit(); - } - saveLayerAsImage(layerNameA, 700, 70); - saveLayerAsImage(layerNameB, 700, 70); - saveLayerAsImage(layerNameC, 700, 70); - - List results = new ArrayList<>(); - inTx(tx -> { - Layer layerA = spatial.getLayer(tx, layerNameA); - Layer layerB = spatial.getLayer(tx, layerNameB); - Layer layerC = spatial.getLayer(tx, layerNameC); - Envelope bboxA = layerA.getIndex().getBoundingBox(tx); - Envelope bboxB = layerB.getIndex().getBoundingBox(tx); - Envelope bboxC = layerC.getIndex().getBoundingBox(tx); - double[] centreA = bboxA.centre(); - double[] centreB = bboxB.centre(); - double[] centreC = bboxC.centre(); - - List resultsA; - List resultsB; - List resultsC; - resultsA = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerA, new Coordinate(centreA[0] + 0.1, centreA[1]), 10.0).toSpatialDatabaseRecordList(); - resultsB = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerB, new Coordinate(centreB[0] + 0.1, centreB[1]), 10.0).toSpatialDatabaseRecordList(); - resultsC = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerC, new Coordinate(centreC[0] + 0.1, centreC[1]), 10.0).toSpatialDatabaseRecordList(); - results.addAll(resultsA); - results.addAll(resultsB); - results.addAll(resultsC); - assertEquals(71, resultsA.size()); - assertEquals(71, resultsB.size()); - assertEquals(71, resultsC.size()); - assertEquals(213, results.size()); - saveResultsAsImage(resultsA, "temporary-results-layer-" + layerA.getName(), 130, 70); - saveResultsAsImage(resultsB, "temporary-results-layer-" + layerB.getName(), 130, 70); - saveResultsAsImage(resultsC, "temporary-results-layer-" + layerC.getName(), 130, 70); - saveResultsAsImage(results, "temporary-results-layer-" + layerA.getName() + "-" + layerB.getName() + "-" + layerC.getName(), 200, 200); - }); - - assertIndexCountSameAs(layerNameA, coords.length); - assertIndexCountSameAs(layerNameB, coords.length); - assertIndexCountSameAs(layerNameC, coords.length); - } - - private void checkPointOrder(List results) { - for (int i = 0; i < results.size() - 1; i++) { - GeoPipeFlow first = results.get(i); - GeoPipeFlow second = results.get(i + 1); - double d1 = (Double) first.getProperties().get(OrthodromicDistance.DISTANCE); - double d2 = (Double) second.getProperties().get(OrthodromicDistance.DISTANCE); - assertTrue(d1 <= d2, "Point at position " + i + " (d=" + d1 + ") must be closer than point at position " + (i + 1) + " (d=" + d2 + ")"); - } - } - - @Test - public void testDensePointLayer() { - String layerName = "neo-dense"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - SimplePointLayer layer = spatial.createSimplePointLayer(tx, layerName, "lon", "lat"); - assertNotNull(layer); - for (Coordinate coordinate : makeDensePointData()) { - Point point = layer.getGeometryFactory().createPoint(coordinate); - SpatialRecord record = layer.add(tx, point); - assertNotNull(record); - } - }); - - saveLayerAsImage(layerName, 300, 300); - - inTx(tx -> { - Layer layer = spatial.getLayer(tx, layerName); - Envelope bbox = layer.getIndex().getBoundingBox(tx); - double[] centre = bbox.centre(); - - List results = GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0], centre[1]), 10.0) - .toSpatialDatabaseRecordList(); - saveResultsAsImage(results, "temporary-results-layer-" + layer.getName(), 150, 150); - assertEquals(456, results.size()); - - // Repeat with sorting - results = GeoPipeline - .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0], centre[1]), 10.0) - .sort(OrthodromicDistance.DISTANCE) - .toSpatialDatabaseRecordList(); - saveResultsAsImage(results, "temporary-results-layer-sorted-" + layer.getName(), 150, 150); - assertEquals(456, results.size()); - }); - } - - private void saveLayerAsImage(String layerName, int width, int height) { - ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); - shpExporter.setExportDir("target/export/SimplePointTests"); - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - imageExporter.setExportDir("target/export/SimplePointTests"); - imageExporter.setZoom(0.9); - imageExporter.setSize(width, height); - try { - imageExporter.saveLayerImage(layerName); - shpExporter.exportLayer(layerName); - } catch (Exception e) { - e.printStackTrace(); - throw new AssertionFailedError("Failed to save layer '" + layerName + "' as image: " + e.getMessage()); - } - } - - private void saveResultsAsImage(List results, String layerName, int width, int height) { - ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); - shpExporter.setExportDir("target/export/SimplePointTests"); - StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); - imageExporter.setExportDir("target/export/SimplePointTests"); - imageExporter.setZoom(0.9); - imageExporter.setSize(width, height); - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - inTx(tx -> { - EditableLayer tmpLayer = spatial.createSimplePointLayer(tx, layerName, "lon", "lat"); - for (SpatialRecord record : results) { - tmpLayer.add(tx, record.getGeometry()); - } - }); - try { - imageExporter.saveLayerImage(layerName); - shpExporter.exportLayer(layerName); - } catch (Exception e) { - throw new AssertionFailedError("Failed to save results image: " + e.getMessage()); - } - } - - @SuppressWarnings({"SameParameterValue"}) - private static Coordinate[] makeCoordinateDataFromTextFile(String textFile, Coordinate origin) { - CoordinateList data = new CoordinateList(); - try { - BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/" + textFile)); - String line; - int row = 0; - while ((line = reader.readLine()) != null) { - int col = 0; - for (String character : line.split("")) { - if (col > 0 && !character.matches("\\s")) { - Coordinate coordinate = new Coordinate(origin.x + (double) col / 100.0, origin.y - (double) row / 100.0); - data.add(coordinate); - } - col++; - } - row++; - } - } catch (IOException e) { - throw new AssertionFailedError("Input data for string test invalid: " + e.getMessage()); - } - return data.toCoordinateArray(); - } - - private static Coordinate[] makeDensePointData() { - CoordinateList data = new CoordinateList(); - Coordinate origin = new Coordinate(13.0, 55.6); - for (int row = 0; row < 40; row++) { - for (int col = 0; col < 40; col++) { - Coordinate coordinate = new Coordinate(origin.x + (double) col / 100.0, origin.y - (double) row / 100.0); - data.add(coordinate); - } - } - return data.toCoordinateArray(); - } - - private void assertIndexCountSameAs(String layerName, int count) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - int indexCount = spatial.getLayer(tx, layerName).getIndex().count(tx); - assertEquals(count, indexCount); - tx.commit(); - } - } - - private void inTx(Consumer txFunction) { - try (Transaction tx = graphDb().beginTx()) { - txFunction.accept(tx); - tx.commit(); - } - } + private static final Coordinate testOrigin = new Coordinate(13.0, 55.6); + + @Test + public void testNearestNeighborSearchOnEmptyLayer() { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = spatial.createSimplePointLayer(tx, layerName, "Longitude", "Latitude"); + assertNotNull(layer); + tx.commit(); + } + + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + // finds geometries around point + List results = GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(15.3, 56.2), 1.0) + .toSpatialDatabaseRecordList(); + + // should find no results + assertEquals(0, results.size()); + tx.commit(); + } + + } + + @Test + public void testSimplePointLayer() { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = spatial.createSimplePointLayer(tx, layerName, "Longitude", "Latitude"); + assertNotNull(layer); + SpatialRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); + assertNotNull(record); + tx.commit(); + } + + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + // finds geometries that contain the given geometry + Geometry geometry = layer.getGeometryFactory() + .toGeometry(new org.locationtech.jts.geom.Envelope(15.0, 16.0, 56.0, 57.0)); + List results = GeoPipeline.startContainSearch(tx, layer, geometry) + .toSpatialDatabaseRecordList(); + + // should not be contained + assertEquals(0, results.size()); + + results = GeoPipeline.startWithinSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); + + assertEquals(1, results.size()); + tx.commit(); + } + } + + @Test + public void testNativePointLayer() { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer layer = spatial.createNativePointLayer(tx, layerName, "location"); + assertNotNull(layer); + SpatialRecord record = layer.add(tx, layer.getGeometryFactory().createPoint(new Coordinate(15.3, 56.2))); + assertNotNull(record); + }); + + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = (EditableLayer) spatial.getLayer(tx, layerName); + // finds geometries that contain the given geometry + Geometry geometry = layer.getGeometryFactory() + .toGeometry(new org.locationtech.jts.geom.Envelope(15.0, 16.0, 56.0, 57.0)); + List results = GeoPipeline.startContainSearch(tx, layer, geometry) + .toSpatialDatabaseRecordList(); + + // should not be contained + assertEquals(0, results.size()); + + results = GeoPipeline.startWithinSearch(tx, layer, geometry).toSpatialDatabaseRecordList(); + + assertEquals(1, results.size()); + tx.commit(); + } + } + + @Test + public void testNeoTextLayer() { + String layerName = "neo-text"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + SimplePointLayer layer = spatial.createSimplePointLayer(tx, layerName); + assertNotNull(layer); + assertNotNull(layer.getName(), "layer name is not null"); + for (Coordinate coordinate : makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin)) { + SpatialRecord record = layer.add(tx, coordinate); + assertNotNull(record); + } + }); + saveLayerAsImage(layerName, 700, 70); + + try (Transaction tx = graphDb().beginTx()) { + SimplePointLayer layer = (SimplePointLayer) spatial.getLayer(tx, layerName); + Envelope bbox = layer.getIndex().getBoundingBox(tx); + double[] centre = bbox.centre(); + + List results = GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0] + 0.1, centre[1]), 10.0) + .sort(OrthodromicDistance.DISTANCE).toList(); + + saveResultsAsImage(results, "temporary-results-layer-" + layer.getName(), 130, 70); + assertEquals(71, results.size()); + checkPointOrder(results); + + results = GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0] + 0.1, centre[1]), 5.0) + .sort(OrthodromicDistance.DISTANCE).toList(); + + saveResultsAsImage(results, "temporary-results-layer2-" + layer.getName(), 130, 70); + assertEquals(30, results.size()); + checkPointOrder(results); + + // Now test the old API + results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 10.0); + assertEquals(71, results.size()); + checkPointOrder(results); + results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 1000); + assertEquals(265, results.size()); // There are only 265 points in dataset + checkPointOrder(results); + results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1]), 100); + assertEquals(100, + results.size()); // We expect an exact count from the layer method (but not from the pipeline) + checkPointOrder(results); + results = layer.findClosestPointsTo(tx, new Coordinate(centre[0] + 0.1, centre[1])); + assertEquals(100, results.size()); // The default in SimplePointLayer is 100 results + checkPointOrder(results); + tx.commit(); + } + + } + + @Test + public void testIndexingExistingSimplePointNodes() { + String layerName = "my-simple-points"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> spatial.createSimplePointLayer(tx, layerName, "x", "y")); + + Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); + inTx(tx -> { + Layer layer = spatial.getLayer(tx, layerName); + for (Coordinate coordinate : coords) { + Node n = tx.createNode(); + n.setProperty("x", coordinate.x); + n.setProperty("y", coordinate.y); + layer.add(tx, n); + } + }); + saveLayerAsImage(layerName, 700, 70); + assertIndexCountSameAs(layerName, coords.length); + } + + @Test + public void testIndexingExistingNativePointNodes() { + String layerName = "my-native-points"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> spatial.createNativePointLayer(tx, "my-native-points", "position")); + Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); + + Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); + inTx(tx -> { + Layer layer = spatial.getLayer(tx, layerName); + for (Coordinate coordinate : coords) { + Node n = tx.createNode(); + n.setProperty("x", coordinate.x); + n.setProperty("y", coordinate.y); + n.setProperty("position", new Neo4jPoint(coordinate, crs)); + layer.add(tx, n); + } + }); + saveLayerAsImage(layerName, 700, 70); + assertIndexCountSameAs(layerName, coords.length); + } + + @Test + public void testIndexingExistingPointNodesWithMultipleLocations() { + String layerNameA = "my-points-A"; + String layerNameB = "my-points-B"; + String layerNameC = "my-points-C"; + GraphDatabaseService db = graphDb(); + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + double x_offset = 0.15, y_offset = 0.15; + inTx(tx -> { + spatial.createSimplePointLayer(tx, layerNameA, "xa", "ya", "bbox_a"); + spatial.createSimplePointLayer(tx, layerNameB, "xb", "yb", "bbox_b"); + spatial.createNativePointLayer(tx, layerNameC, "loc", "bbox_c"); + }); + Neo4jCRS crs = Neo4jCRS.findCRS("WGS-84"); + + Coordinate[] coords = makeCoordinateDataFromTextFile("NEO4J-SPATIAL.txt", testOrigin); + try (Transaction tx = db.beginTx()) { + Layer layerA = spatial.getLayer(tx, layerNameA); + Layer layerB = spatial.getLayer(tx, layerNameB); + Layer layerC = spatial.getLayer(tx, layerNameC); + for (Coordinate coordinate : coords) { + Node n = tx.createNode(); + n.setProperty("xa", coordinate.x); + n.setProperty("ya", coordinate.y); + n.setProperty("xb", coordinate.x + x_offset); + n.setProperty("yb", coordinate.y + y_offset); + n.setProperty("loc", + new Neo4jPoint(new double[]{coordinate.x + 2 * x_offset, coordinate.y + 2 * y_offset}, crs)); + + layerA.add(tx, n); + layerB.add(tx, n); + layerC.add(tx, n); + } + tx.commit(); + } + saveLayerAsImage(layerNameA, 700, 70); + saveLayerAsImage(layerNameB, 700, 70); + saveLayerAsImage(layerNameC, 700, 70); + + List results = new ArrayList<>(); + inTx(tx -> { + Layer layerA = spatial.getLayer(tx, layerNameA); + Layer layerB = spatial.getLayer(tx, layerNameB); + Layer layerC = spatial.getLayer(tx, layerNameC); + Envelope bboxA = layerA.getIndex().getBoundingBox(tx); + Envelope bboxB = layerB.getIndex().getBoundingBox(tx); + Envelope bboxC = layerC.getIndex().getBoundingBox(tx); + double[] centreA = bboxA.centre(); + double[] centreB = bboxB.centre(); + double[] centreC = bboxC.centre(); + + List resultsA; + List resultsB; + List resultsC; + resultsA = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerA, + new Coordinate(centreA[0] + 0.1, centreA[1]), 10.0).toSpatialDatabaseRecordList(); + resultsB = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerB, + new Coordinate(centreB[0] + 0.1, centreB[1]), 10.0).toSpatialDatabaseRecordList(); + resultsC = GeoPipeline.startNearestNeighborLatLonSearch(tx, layerC, + new Coordinate(centreC[0] + 0.1, centreC[1]), 10.0).toSpatialDatabaseRecordList(); + results.addAll(resultsA); + results.addAll(resultsB); + results.addAll(resultsC); + assertEquals(71, resultsA.size()); + assertEquals(71, resultsB.size()); + assertEquals(71, resultsC.size()); + assertEquals(213, results.size()); + saveResultsAsImage(resultsA, "temporary-results-layer-" + layerA.getName(), 130, 70); + saveResultsAsImage(resultsB, "temporary-results-layer-" + layerB.getName(), 130, 70); + saveResultsAsImage(resultsC, "temporary-results-layer-" + layerC.getName(), 130, 70); + saveResultsAsImage(results, + "temporary-results-layer-" + layerA.getName() + "-" + layerB.getName() + "-" + layerC.getName(), + 200, 200); + }); + + assertIndexCountSameAs(layerNameA, coords.length); + assertIndexCountSameAs(layerNameB, coords.length); + assertIndexCountSameAs(layerNameC, coords.length); + } + + private void checkPointOrder(List results) { + for (int i = 0; i < results.size() - 1; i++) { + GeoPipeFlow first = results.get(i); + GeoPipeFlow second = results.get(i + 1); + double d1 = (Double) first.getProperties().get(OrthodromicDistance.DISTANCE); + double d2 = (Double) second.getProperties().get(OrthodromicDistance.DISTANCE); + assertTrue(d1 <= d2, + "Point at position " + i + " (d=" + d1 + ") must be closer than point at position " + (i + 1) + + " (d=" + d2 + ")"); + } + } + + @Test + public void testDensePointLayer() { + String layerName = "neo-dense"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + SimplePointLayer layer = spatial.createSimplePointLayer(tx, layerName, "lon", "lat"); + assertNotNull(layer); + for (Coordinate coordinate : makeDensePointData()) { + Point point = layer.getGeometryFactory().createPoint(coordinate); + SpatialRecord record = layer.add(tx, point); + assertNotNull(record); + } + }); + + saveLayerAsImage(layerName, 300, 300); + + inTx(tx -> { + Layer layer = spatial.getLayer(tx, layerName); + Envelope bbox = layer.getIndex().getBoundingBox(tx); + double[] centre = bbox.centre(); + + List results = GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0], centre[1]), 10.0) + .toSpatialDatabaseRecordList(); + saveResultsAsImage(results, "temporary-results-layer-" + layer.getName(), 150, 150); + assertEquals(456, results.size()); + + // Repeat with sorting + results = GeoPipeline + .startNearestNeighborLatLonSearch(tx, layer, new Coordinate(centre[0], centre[1]), 10.0) + .sort(OrthodromicDistance.DISTANCE) + .toSpatialDatabaseRecordList(); + saveResultsAsImage(results, "temporary-results-layer-sorted-" + layer.getName(), 150, 150); + assertEquals(456, results.size()); + }); + } + + private void saveLayerAsImage(String layerName, int width, int height) { + ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); + shpExporter.setExportDir("target/export/SimplePointTests"); + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + imageExporter.setExportDir("target/export/SimplePointTests"); + imageExporter.setZoom(0.9); + imageExporter.setSize(width, height); + try { + imageExporter.saveLayerImage(layerName); + shpExporter.exportLayer(layerName); + } catch (Exception e) { + e.printStackTrace(); + throw new AssertionFailedError("Failed to save layer '" + layerName + "' as image: " + e.getMessage()); + } + } + + private void saveResultsAsImage(List results, String layerName, int width, int height) { + ShapefileExporter shpExporter = new ShapefileExporter(graphDb()); + shpExporter.setExportDir("target/export/SimplePointTests"); + StyledImageExporter imageExporter = new StyledImageExporter(graphDb()); + imageExporter.setExportDir("target/export/SimplePointTests"); + imageExporter.setZoom(0.9); + imageExporter.setSize(width, height); + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + inTx(tx -> { + EditableLayer tmpLayer = spatial.createSimplePointLayer(tx, layerName, "lon", "lat"); + for (SpatialRecord record : results) { + tmpLayer.add(tx, record.getGeometry()); + } + }); + try { + imageExporter.saveLayerImage(layerName); + shpExporter.exportLayer(layerName); + } catch (Exception e) { + throw new AssertionFailedError("Failed to save results image: " + e.getMessage()); + } + } + + @SuppressWarnings({"SameParameterValue"}) + private static Coordinate[] makeCoordinateDataFromTextFile(String textFile, Coordinate origin) { + CoordinateList data = new CoordinateList(); + try { + BufferedReader reader = new BufferedReader(new FileReader("src/main/resources/" + textFile)); + String line; + int row = 0; + while ((line = reader.readLine()) != null) { + int col = 0; + for (String character : line.split("")) { + if (col > 0 && !character.matches("\\s")) { + Coordinate coordinate = new Coordinate(origin.x + (double) col / 100.0, + origin.y - (double) row / 100.0); + data.add(coordinate); + } + col++; + } + row++; + } + } catch (IOException e) { + throw new AssertionFailedError("Input data for string test invalid: " + e.getMessage()); + } + return data.toCoordinateArray(); + } + + private static Coordinate[] makeDensePointData() { + CoordinateList data = new CoordinateList(); + Coordinate origin = new Coordinate(13.0, 55.6); + for (int row = 0; row < 40; row++) { + for (int col = 0; col < 40; col++) { + Coordinate coordinate = new Coordinate(origin.x + (double) col / 100.0, + origin.y - (double) row / 100.0); + data.add(coordinate); + } + } + return data.toCoordinateArray(); + } + + private void assertIndexCountSameAs(String layerName, int count) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + int indexCount = spatial.getLayer(tx, layerName).getIndex().count(tx); + assertEquals(count, indexCount); + tx.commit(); + } + } + + private void inTx(Consumer txFunction) { + try (Transaction tx = graphDb().beginTx()) { + txFunction.accept(tx); + tx.commit(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestSpatial.java b/src/test/java/org/neo4j/gis/spatial/TestSpatial.java index 43c4b1243..753ef3c35 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestSpatial.java +++ b/src/test/java/org/neo4j/gis/spatial/TestSpatial.java @@ -19,6 +19,13 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,14 +43,6 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; - -import static org.junit.jupiter.api.Assertions.fail; - /** *

* Test cases for initial version of Neo4j-Spatial. This was originally @@ -81,346 +80,376 @@ */ public class TestSpatial extends Neo4jTestCase { - private final ArrayList layers = new ArrayList<>(); - private final HashMap layerTestEnvelope = new HashMap<>(); - private final HashMap> layerTestGeometries = new HashMap<>(); - private final HashMap layerTestFormats = new HashMap<>(); - private final HashMap geomStats = new HashMap<>(); - private final String spatialTestMode = System.getProperty("spatial.test.mode"); - - @BeforeEach - public void setUp() throws Exception { - super.setUp(); - - // TODO: Rather load this from a configuration file, properties file or JRuby test code - - Envelope bbox = new Envelope(12.9, 12.99, 56.05, 56.07); // covers half of Billesholm - - addTestLayer("billesholm.osm", DataFormat.OSM, bbox); - addTestGeometry(70423036, "Ljungsgårdsvägen", "outside top left", "(12.9599540,56.0570692), (12.9624780,56.0716282)"); - addTestGeometry(67835020, "Villagatan", "in the middle", "(12.9776065,56.0561477), (12.9814421,56.0572131)"); - addTestGeometry(60966388, "Storgatan", "crossing left edge", "(12.9682980,56.0524546), (12.9710302,56.0538436)"); - - bbox = new Envelope(12.5, 14.1, 55.0, 56.3); // cover central Skåne - // Bounds for sweden_administrative: 11.1194502 : 24.1585511, 55.3550515 : 69.0600767 - // Envelope bbox = new Envelope(12.85, 13.25, 55.5, 55.65); // cover Malmö - // Envelope bbox = new Envelope(13, 14, 55, 58); // cover admin area 'Söderåsen' - // Envelope bbox = new Envelope(7, 10, 37, 40); - - addTestLayer("sweden.osm", DataFormat.OSM, bbox); - addTestLayer("sweden.osm.administrative", DataFormat.OSM, bbox); - - addTestLayer("sweden_administrative", DataFormat.SHP, bbox); - addTestGeometry(1055, "Söderåsens nationalpark", "near top edge", "(13.167721,56.002416), (13.289724,56.047099)"); - addTestGeometry(1067, "", "inside", "(13.2122907,55.6969478), (13.5614499,55.7835819)"); - addTestGeometry(943, "", "crosses left edge", "(12.9120438,55.8253138), (13.0501381,55.8484289)"); - addTestGeometry(884, "", "outside left", "(12.7492433,55.9269403), (12.9503304,55.964951)"); - addTestGeometry(1362, "", "crosses top right", "(13.7453871,55.9483067), (14.0084487,56.1538786)"); - addTestGeometry(1521, "", "outside right", "(14.0762394,55.4889569), (14.1869043,55.7592587)"); - addTestGeometry(1184, "", "outside above", "(13.4215555,56.109138), (13.4683671,56.2681347)"); - - addTestLayer("sweden_natural", DataFormat.SHP, bbox); - addTestGeometry(208, "Bokskogen", "", "(13.1935576,55.5324763), (13.2710125,55.5657891)"); - addTestGeometry(3462, "Pålsjö skog", "", "(12.6746748,56.0634246), (12.6934147,56.0776016)"); - addTestGeometry(647, "Dalby söderskog", "", "(13.32406,55.671652), (13.336948,55.679243)"); - - addTestLayer("sweden_water", DataFormat.SHP, bbox); - addTestGeometry(13149, "Yddingesjön", "", "(13.23564,55.5360264), (13.2676649,55.5558856)"); - addTestGeometry(14431, "Finjasjön", "", "(13.6718979,56.1157516), (13.7398759,56.1566911)"); - - // TODO missing file - addTestLayer("sweden_highway", DataFormat.SHP, bbox); - addTestGeometry(58904, "Holmeja byväg", "", "(13.2819022,55.5561414), (13.2820848,55.5575418)"); - addTestGeometry(45305, "Yttre RIngvägen", "", "(12.9827334,55.5473645), (13.0118313,55.5480455)"); - addTestGeometry(43536, "Yttre RIngvägen", "", "(12.9412071,55.5564264), (12.9422181,55.5571701)"); - } - - private void addTestLayer(String layer, DataFormat format, Envelope bbox) { - layers.add(layer); - layerTestEnvelope.put(layer, bbox); - layerTestFormats.put(layer, format); - layerTestGeometries.put(layer, new ArrayList<>()); - } - - private void addTestGeometry(Integer id, String name, String comments, String bounds) { - String layer = layers.get(layers.size() - 1); - ArrayList geoms = layerTestGeometries.get(layer); - geoms.add(new TestGeometry(id, name, comments, bounds)); - } - - @Test - public void testShpSwedenAdministrative() throws Exception { - if ("long".equals(spatialTestMode)) - testLayer("sweden_administrative"); - } - - @Test - public void testShpSwedenNatural() throws Exception { - if ("long".equals(spatialTestMode)) - testLayer("sweden_natural"); - } - - @Test - public void testShpSwedenWater() throws Exception { - if ("long".equals(spatialTestMode)) - testLayer("sweden_water"); - } - - @Test - public void testOsmBillesholm() throws Exception { - testLayer("billesholm.osm"); - } - - @Test - public void testOsmSwedenAdministrative() throws Exception { - if ("long".equals(spatialTestMode)) - testLayer("sweden.osm.administrative"); - } - - @Test - public void testOsmSweden() throws Exception { - if ("long".equals(spatialTestMode)) - testLayer("sweden.osm"); - } - - // TODO missing file - /* + private final ArrayList layers = new ArrayList<>(); + private final HashMap layerTestEnvelope = new HashMap<>(); + private final HashMap> layerTestGeometries = new HashMap<>(); + private final HashMap layerTestFormats = new HashMap<>(); + private final HashMap geomStats = new HashMap<>(); + private final String spatialTestMode = System.getProperty("spatial.test.mode"); + + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + // TODO: Rather load this from a configuration file, properties file or JRuby test code + + Envelope bbox = new Envelope(12.9, 12.99, 56.05, 56.07); // covers half of Billesholm + + addTestLayer("billesholm.osm", DataFormat.OSM, bbox); + addTestGeometry(70423036, "Ljungsgårdsvägen", "outside top left", + "(12.9599540,56.0570692), (12.9624780,56.0716282)"); + addTestGeometry(67835020, "Villagatan", "in the middle", "(12.9776065,56.0561477), (12.9814421,56.0572131)"); + addTestGeometry(60966388, "Storgatan", "crossing left edge", + "(12.9682980,56.0524546), (12.9710302,56.0538436)"); + + bbox = new Envelope(12.5, 14.1, 55.0, 56.3); // cover central Skåne + // Bounds for sweden_administrative: 11.1194502 : 24.1585511, 55.3550515 : 69.0600767 + // Envelope bbox = new Envelope(12.85, 13.25, 55.5, 55.65); // cover Malmö + // Envelope bbox = new Envelope(13, 14, 55, 58); // cover admin area 'Söderåsen' + // Envelope bbox = new Envelope(7, 10, 37, 40); + + addTestLayer("sweden.osm", DataFormat.OSM, bbox); + addTestLayer("sweden.osm.administrative", DataFormat.OSM, bbox); + + addTestLayer("sweden_administrative", DataFormat.SHP, bbox); + addTestGeometry(1055, "Söderåsens nationalpark", "near top edge", + "(13.167721,56.002416), (13.289724,56.047099)"); + addTestGeometry(1067, "", "inside", "(13.2122907,55.6969478), (13.5614499,55.7835819)"); + addTestGeometry(943, "", "crosses left edge", "(12.9120438,55.8253138), (13.0501381,55.8484289)"); + addTestGeometry(884, "", "outside left", "(12.7492433,55.9269403), (12.9503304,55.964951)"); + addTestGeometry(1362, "", "crosses top right", "(13.7453871,55.9483067), (14.0084487,56.1538786)"); + addTestGeometry(1521, "", "outside right", "(14.0762394,55.4889569), (14.1869043,55.7592587)"); + addTestGeometry(1184, "", "outside above", "(13.4215555,56.109138), (13.4683671,56.2681347)"); + + addTestLayer("sweden_natural", DataFormat.SHP, bbox); + addTestGeometry(208, "Bokskogen", "", "(13.1935576,55.5324763), (13.2710125,55.5657891)"); + addTestGeometry(3462, "Pålsjö skog", "", "(12.6746748,56.0634246), (12.6934147,56.0776016)"); + addTestGeometry(647, "Dalby söderskog", "", "(13.32406,55.671652), (13.336948,55.679243)"); + + addTestLayer("sweden_water", DataFormat.SHP, bbox); + addTestGeometry(13149, "Yddingesjön", "", "(13.23564,55.5360264), (13.2676649,55.5558856)"); + addTestGeometry(14431, "Finjasjön", "", "(13.6718979,56.1157516), (13.7398759,56.1566911)"); + + // TODO missing file + addTestLayer("sweden_highway", DataFormat.SHP, bbox); + addTestGeometry(58904, "Holmeja byväg", "", "(13.2819022,55.5561414), (13.2820848,55.5575418)"); + addTestGeometry(45305, "Yttre RIngvägen", "", "(12.9827334,55.5473645), (13.0118313,55.5480455)"); + addTestGeometry(43536, "Yttre RIngvägen", "", "(12.9412071,55.5564264), (12.9422181,55.5571701)"); + } + + private void addTestLayer(String layer, DataFormat format, Envelope bbox) { + layers.add(layer); + layerTestEnvelope.put(layer, bbox); + layerTestFormats.put(layer, format); + layerTestGeometries.put(layer, new ArrayList<>()); + } + + private void addTestGeometry(Integer id, String name, String comments, String bounds) { + String layer = layers.get(layers.size() - 1); + ArrayList geoms = layerTestGeometries.get(layer); + geoms.add(new TestGeometry(id, name, comments, bounds)); + } + + @Test + public void testShpSwedenAdministrative() throws Exception { + if ("long".equals(spatialTestMode)) { + testLayer("sweden_administrative"); + } + } + + @Test + public void testShpSwedenNatural() throws Exception { + if ("long".equals(spatialTestMode)) { + testLayer("sweden_natural"); + } + } + + @Test + public void testShpSwedenWater() throws Exception { + if ("long".equals(spatialTestMode)) { + testLayer("sweden_water"); + } + } + + @Test + public void testOsmBillesholm() throws Exception { + testLayer("billesholm.osm"); + } + + @Test + public void testOsmSwedenAdministrative() throws Exception { + if ("long".equals(spatialTestMode)) { + testLayer("sweden.osm.administrative"); + } + } + + @Test + public void testOsmSweden() throws Exception { + if ("long".equals(spatialTestMode)) { + testLayer("sweden.osm"); + } + } + + // TODO missing file + /* public void testShpSwedenHighway() throws Exception { if ("long".equals(spatialTestMode)) testLayer("sweden_highway"); } */ - private void testLayer(String layerName) throws Exception { - testImport(layerName); - testSpatialIndex(layerName); - } - - private long countLayerIndex(String layerName) { - long count = 0; - try (Transaction tx = graphDb().beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - Layer layer = spatial.getLayer(tx, layerName); - if (layer != null && layer.getIndex() != null) { - count = layer.getIndex().count(tx); - } - tx.commit(); - } - return count; - } - - private void testImport(String layerName) throws Exception { - long start = System.currentTimeMillis(); - System.out.println("\n===========\n=========== Import Test: " + layerName + "\n==========="); - if (countLayerIndex(layerName) < 1) { - switch (layerTestFormats.get(layerName)) { - case SHP: - loadTestShpData(layerName); - break; - case OSM: - //TODO: enable batch again - loadTestOsmData(layerName); - break; - default: - fail("Unknown format: " + layerTestFormats.get(layerName)); - } - } else { - fail("Layer already present: " + layerName); - } - - System.out.println("Total time for load: " + 1.0 * (System.currentTimeMillis() - start) / 1000.0 + "s"); - } - - private void loadTestShpData(String layerName) throws IOException { - String SHP_DIR = "target/shp"; - String shpPath = SHP_DIR + File.separator + layerName; - System.out.println("\n=== Loading layer " + layerName + " from " + shpPath + " ==="); - ShapefileImporter importer = new ShapefileImporter(graphDb(), new NullListener(), 1000, true); - importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); - } - - private void loadTestOsmData(String layerName) throws Exception { - String OSM_DIR = "target/osm"; - String osmPath = OSM_DIR + File.separator + layerName; - System.out.println("\n=== Loading layer " + layerName + " from " + osmPath + " ==="); - OSMImporter importer = new OSMImporter(layerName); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(graphDb(), osmPath); - importer.reIndex(graphDb(), 1000); - } - - private void testSpatialIndex(String layerName) { - System.out.println("\n=== Spatial Index Test: " + layerName + " ==="); - long start = System.currentTimeMillis(); - - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - if (layer == null || layer.getIndex() == null || layer.getIndex().count(tx) < 1) { - fail("Layer not loaded: " + layerName); - } - OSMDataset.fromLayer(tx, (OSMLayer) layer); // force lookup - - LayerIndexReader fakeIndex = new SpatialIndexPerformanceProxy(new FakeIndex(layer, spatial.indexManager)); - LayerIndexReader rtreeIndex = new SpatialIndexPerformanceProxy(layer.getIndex()); - - System.out.println("RTreeIndex bounds: " + rtreeIndex.getBoundingBox(tx)); - System.out.println("FakeIndex bounds: " + fakeIndex.getBoundingBox(tx)); - assertEnvelopeEquals(fakeIndex.getBoundingBox(tx), rtreeIndex.getBoundingBox(tx)); - - System.out.println("RTreeIndex count: " + rtreeIndex.count(tx)); - Assertions.assertEquals(fakeIndex.count(tx), rtreeIndex.count(tx)); - - Envelope bbox = layerTestEnvelope.get(layerName); - - System.out.println("Displaying test geometries for layer '" + layerName + "' including expected search results"); - for (TestGeometry testData : layerTestGeometries.get(layerName)) { - System.out.println("\tGeometry: " + testData + " " + (testData.inOrIntersects(bbox) ? "is" : "is NOT") + " inside search region"); - } - - for (LayerIndexReader index : new LayerIndexReader[]{rtreeIndex, fakeIndex}) { - ArrayList foundData = new ArrayList<>(); - - SearchIntersect searchQuery = new SearchIntersect(layer, layer.getGeometryFactory().toGeometry(Utilities.fromNeo4jToJts(bbox))); - SearchRecords results = index.search(tx, searchQuery); - - int count = 0; - int ri = 0; - for (SpatialDatabaseRecord r : results) { - count++; - if (ri++ < 10) { - StringBuilder props = new StringBuilder(); - for (String prop : r.getPropertyNames(tx)) { - if (props.length() > 0) props.append(", "); - props.append(prop).append(": ").append(r.getProperty(tx, prop)); - } - - System.out.println("\tRTreeIndex result[" + ri + "]: " + r.getNodeId() + ":" + r.getType() + " - " + r + ": PROPS[" + props + "]"); - } else if (ri == 10) { - System.out.println("\t.. and " + (count - ri) + " more .."); - } - - addGeomStats(r.getGeomNode()); - - String name = (String) r.getProperty(tx, "NAME"); - if (name == null) name = (String) r.getProperty(tx, "name"); - - Integer id = (Integer) r.getProperty(tx, "ID"); - if ((name != null && name.length() > 0) || id != null) { - for (TestGeometry testData : layerTestGeometries.get(layerName)) - if ((name != null && name.length() > 0 && testData.name.equals(name)) || (testData.id.equals(id))) { - System.out.println("\tFound match in test data: test[" + testData + "] == result[" + r + "]"); - foundData.add(testData); - } /* else if(name != null && name.length()>0 && name.startsWith(testData.name.substring(0,1))) { + private void testLayer(String layerName) throws Exception { + testImport(layerName); + testSpatialIndex(layerName); + } + + private long countLayerIndex(String layerName) { + long count = 0; + try (Transaction tx = graphDb().beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + Layer layer = spatial.getLayer(tx, layerName); + if (layer != null && layer.getIndex() != null) { + count = layer.getIndex().count(tx); + } + tx.commit(); + } + return count; + } + + private void testImport(String layerName) throws Exception { + long start = System.currentTimeMillis(); + System.out.println("\n===========\n=========== Import Test: " + layerName + "\n==========="); + if (countLayerIndex(layerName) < 1) { + switch (layerTestFormats.get(layerName)) { + case SHP: + loadTestShpData(layerName); + break; + case OSM: + //TODO: enable batch again + loadTestOsmData(layerName); + break; + default: + fail("Unknown format: " + layerTestFormats.get(layerName)); + } + } else { + fail("Layer already present: " + layerName); + } + + System.out.println("Total time for load: " + 1.0 * (System.currentTimeMillis() - start) / 1000.0 + "s"); + } + + private void loadTestShpData(String layerName) throws IOException { + String SHP_DIR = "target/shp"; + String shpPath = SHP_DIR + File.separator + layerName; + System.out.println("\n=== Loading layer " + layerName + " from " + shpPath + " ==="); + ShapefileImporter importer = new ShapefileImporter(graphDb(), new NullListener(), 1000, true); + importer.importFile(shpPath, layerName, StandardCharsets.UTF_8); + } + + private void loadTestOsmData(String layerName) throws Exception { + String OSM_DIR = "target/osm"; + String osmPath = OSM_DIR + File.separator + layerName; + System.out.println("\n=== Loading layer " + layerName + " from " + osmPath + " ==="); + OSMImporter importer = new OSMImporter(layerName); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(graphDb(), osmPath); + importer.reIndex(graphDb(), 1000); + } + + private void testSpatialIndex(String layerName) { + System.out.println("\n=== Spatial Index Test: " + layerName + " ==="); + long start = System.currentTimeMillis(); + + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + if (layer == null || layer.getIndex() == null || layer.getIndex().count(tx) < 1) { + fail("Layer not loaded: " + layerName); + } + OSMDataset.fromLayer(tx, (OSMLayer) layer); // force lookup + + LayerIndexReader fakeIndex = new SpatialIndexPerformanceProxy(new FakeIndex(layer, spatial.indexManager)); + LayerIndexReader rtreeIndex = new SpatialIndexPerformanceProxy(layer.getIndex()); + + System.out.println("RTreeIndex bounds: " + rtreeIndex.getBoundingBox(tx)); + System.out.println("FakeIndex bounds: " + fakeIndex.getBoundingBox(tx)); + assertEnvelopeEquals(fakeIndex.getBoundingBox(tx), rtreeIndex.getBoundingBox(tx)); + + System.out.println("RTreeIndex count: " + rtreeIndex.count(tx)); + Assertions.assertEquals(fakeIndex.count(tx), rtreeIndex.count(tx)); + + Envelope bbox = layerTestEnvelope.get(layerName); + + System.out.println( + "Displaying test geometries for layer '" + layerName + "' including expected search results"); + for (TestGeometry testData : layerTestGeometries.get(layerName)) { + System.out.println("\tGeometry: " + testData + " " + (testData.inOrIntersects(bbox) ? "is" : "is NOT") + + " inside search region"); + } + + for (LayerIndexReader index : new LayerIndexReader[]{rtreeIndex, fakeIndex}) { + ArrayList foundData = new ArrayList<>(); + + SearchIntersect searchQuery = new SearchIntersect(layer, + layer.getGeometryFactory().toGeometry(Utilities.fromNeo4jToJts(bbox))); + SearchRecords results = index.search(tx, searchQuery); + + int count = 0; + int ri = 0; + for (SpatialDatabaseRecord r : results) { + count++; + if (ri++ < 10) { + StringBuilder props = new StringBuilder(); + for (String prop : r.getPropertyNames(tx)) { + if (props.length() > 0) { + props.append(", "); + } + props.append(prop).append(": ").append(r.getProperty(tx, prop)); + } + + System.out.println( + "\tRTreeIndex result[" + ri + "]: " + r.getNodeId() + ":" + r.getType() + " - " + r + + ": PROPS[" + props + "]"); + } else if (ri == 10) { + System.out.println("\t.. and " + (count - ri) + " more .."); + } + + addGeomStats(r.getGeomNode()); + + String name = (String) r.getProperty(tx, "NAME"); + if (name == null) { + name = (String) r.getProperty(tx, "name"); + } + + Integer id = (Integer) r.getProperty(tx, "ID"); + if ((name != null && name.length() > 0) || id != null) { + for (TestGeometry testData : layerTestGeometries.get(layerName)) { + if ((name != null && name.length() > 0 && testData.name.equals(name)) + || (testData.id.equals(id))) { + System.out.println( + "\tFound match in test data: test[" + testData + "] == result[" + r + "]"); + foundData.add(testData); + } /* else if(name != null && name.length()>0 && name.startsWith(testData.name.substring(0,1))) { System.out.println("\tOnly first character matched: test[" + testData + "] == result[" + r + "]"); } */ - } else { - System.err.println("\tNo name or id in RTreeIndex result: " + r.getNodeId() + ":" + r.getType() + " - " + r); - } - } - - dumpGeomStats(); - - System.out.println("Found " + foundData.size() + " test datasets in region[" + bbox + "]"); - for (TestGeometry testData : foundData) { - System.out.println("\t" + testData + ": " + testData.bounds); - } - - System.out.println("Verifying results for " + layerTestGeometries.size() + " test datasets in region[" + bbox + "]"); - for (TestGeometry testData : layerTestGeometries.get(layerName)) { - System.out.println("\ttesting " + testData + ": " + testData.bounds); - if (testData.inOrIntersects(bbox) && !foundData.contains(testData)) { - String error = "Incorrect test result: test[" + testData + "] not found by search inside region[" + bbox + "]"; - for (TestGeometry data : foundData) { - System.out.println(data); - } - System.out.println(error); - fail(error); - } - } - } - - System.out.println("Total time for index test: " + 1.0 * (System.currentTimeMillis() - start) / 1000.0 + "s"); - tx.commit(); - } - } - - private void assertEnvelopeEquals(Envelope a, Envelope b) { - Assertions.assertNotNull(a); - Assertions.assertNotNull(b); - Assertions.assertEquals(a.getDimension(), b.getDimension()); - - for (int i = 0; i < a.getDimension(); i++) { - Assertions.assertEquals(a.getMin(i), b.getMin(i), 0); - Assertions.assertEquals(a.getMax(i), b.getMax(i), 0); - } - } - - private void addGeomStats(Node geomNode) { - addGeomStats((Integer) geomNode.getProperty(Constants.PROP_TYPE, null)); - } - - private void addGeomStats(Integer geom) { - Integer count = geomStats.get(geom); - geomStats.put(geom, count == null ? 1 : count + 1); - } - - private void dumpGeomStats() { - System.out.println("Geometry statistics for " + geomStats.size() + " geometry types:"); - for (Integer key : geomStats.keySet()) { - Integer count = geomStats.get(key); - System.out.println("\t" + SpatialDatabaseService.convertGeometryTypeToName(key) + ": " + count); - } - geomStats.clear(); - } - - private enum DataFormat { - SHP("ESRI Shapefile"), OSM("OpenStreetMap"); - - private final String description; - - DataFormat(String description) { - this.description = description; - } - - @Override - public String toString() { - return description; - } - } - - /** - * This class represents mock objects for representing geometries in simple form in memory for - * testing against real geometries. We have a few hard-coded test geometries we expect to find - * stored in predictable ways in the test database. Currently we only test for bounding box so - * this class only contains that information. - */ - private static class TestGeometry { - - private final Integer id; - private final String name; - private final String comments; - protected Envelope bounds; - - public TestGeometry(Integer id, String name, String comments, String bounds) { - this.id = id; - this.name = name == null ? "" : name; - this.comments = comments; - - float[] bf = new float[4]; - int bi = 0; - for (String bound : bounds.replaceAll("[()\\s]+", "").split(",")) { - bf[bi++] = Float.parseFloat(bound); - } - this.bounds = new Envelope(bf[0], bf[2], bf[1], bf[3]); - } - - @Override - public String toString() { - return (name.length() > 0 ? name : "ID[" + id + "]") + (comments == null || comments.length() < 1 ? "" : " (" + comments + ")"); - } - - public boolean inOrIntersects(Envelope env) { - return env.intersects(bounds); - } - } -} \ No newline at end of file + } + } else { + System.err.println( + "\tNo name or id in RTreeIndex result: " + r.getNodeId() + ":" + r.getType() + " - " + + r); + } + } + + dumpGeomStats(); + + System.out.println("Found " + foundData.size() + " test datasets in region[" + bbox + "]"); + for (TestGeometry testData : foundData) { + System.out.println("\t" + testData + ": " + testData.bounds); + } + + System.out.println( + "Verifying results for " + layerTestGeometries.size() + " test datasets in region[" + bbox + + "]"); + for (TestGeometry testData : layerTestGeometries.get(layerName)) { + System.out.println("\ttesting " + testData + ": " + testData.bounds); + if (testData.inOrIntersects(bbox) && !foundData.contains(testData)) { + String error = + "Incorrect test result: test[" + testData + "] not found by search inside region[" + + bbox + "]"; + for (TestGeometry data : foundData) { + System.out.println(data); + } + System.out.println(error); + fail(error); + } + } + } + + System.out.println( + "Total time for index test: " + 1.0 * (System.currentTimeMillis() - start) / 1000.0 + "s"); + tx.commit(); + } + } + + private void assertEnvelopeEquals(Envelope a, Envelope b) { + Assertions.assertNotNull(a); + Assertions.assertNotNull(b); + Assertions.assertEquals(a.getDimension(), b.getDimension()); + + for (int i = 0; i < a.getDimension(); i++) { + Assertions.assertEquals(a.getMin(i), b.getMin(i), 0); + Assertions.assertEquals(a.getMax(i), b.getMax(i), 0); + } + } + + private void addGeomStats(Node geomNode) { + addGeomStats((Integer) geomNode.getProperty(Constants.PROP_TYPE, null)); + } + + private void addGeomStats(Integer geom) { + Integer count = geomStats.get(geom); + geomStats.put(geom, count == null ? 1 : count + 1); + } + + private void dumpGeomStats() { + System.out.println("Geometry statistics for " + geomStats.size() + " geometry types:"); + for (Integer key : geomStats.keySet()) { + Integer count = geomStats.get(key); + System.out.println("\t" + SpatialDatabaseService.convertGeometryTypeToName(key) + ": " + count); + } + geomStats.clear(); + } + + private enum DataFormat { + SHP("ESRI Shapefile"), OSM("OpenStreetMap"); + + private final String description; + + DataFormat(String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } + } + + /** + * This class represents mock objects for representing geometries in simple form in memory for + * testing against real geometries. We have a few hard-coded test geometries we expect to find + * stored in predictable ways in the test database. Currently we only test for bounding box so + * this class only contains that information. + */ + private static class TestGeometry { + + private final Integer id; + private final String name; + private final String comments; + protected Envelope bounds; + + public TestGeometry(Integer id, String name, String comments, String bounds) { + this.id = id; + this.name = name == null ? "" : name; + this.comments = comments; + + float[] bf = new float[4]; + int bi = 0; + for (String bound : bounds.replaceAll("[()\\s]+", "").split(",")) { + bf[bi++] = Float.parseFloat(bound); + } + this.bounds = new Envelope(bf[0], bf[2], bf[1], bf[3]); + } + + @Override + public String toString() { + return (name.length() > 0 ? name : "ID[" + id + "]") + (comments == null || comments.length() < 1 ? "" + : " (" + comments + ")"); + } + + public boolean inOrIntersects(Envelope env) { + return env.intersects(bounds); + } + } +} diff --git a/src/test/java/org/neo4j/gis/spatial/TestSpatialQueries.java b/src/test/java/org/neo4j/gis/spatial/TestSpatialQueries.java index d3c4c3c03..5a82d511d 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestSpatialQueries.java +++ b/src/test/java/org/neo4j/gis/spatial/TestSpatialQueries.java @@ -19,6 +19,10 @@ */ package org.neo4j.gis.spatial; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Envelope; @@ -31,102 +35,101 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - public class TestSpatialQueries extends Neo4jTestCase { - /** - * This test case is designed to capture the conditions described in the bug - * report at https://github.com/neo4j/neo4j-spatial/issues/11. There are - * three geometries, one pPoint and two LineStrings, one short and one long. - * The short LineString is closer to the Point, but SearchClosest returns - * the long LineString. - */ - @Test - public void testSearchClosestWithShortLongLineStrings() throws ParseException { - String layerName = "test"; - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - Geometry shortLineString; - Geometry longLineString; - Geometry point; - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = spatial.getOrCreateEditableLayer(tx, layerName, "WKT"); - WKTReader wkt = new WKTReader(layer.getGeometryFactory()); - shortLineString = wkt.read("LINESTRING(16.3493032 48.199882,16.3479487 48.1997337)"); - longLineString = wkt.read("LINESTRING(16.3178388 48.1979135,16.3195494 48.1978011,16.3220815 48.197824,16.3259696 48.1978297,16.3281211 48.1975952,16.3312482 48.1968743,16.3327931 48.1965196,16.3354641 48.1959911,16.3384376 48.1959609,16.3395792 48.1960223,16.3458708 48.1970974,16.3477719 48.1975147,16.348008 48.1975665,16.3505572 48.1984533,16.3535613 48.1994545,16.3559474 48.2011765,16.3567056 48.2025723,16.3571261 48.2038308,16.3578393 48.205176)"); - point = wkt.read("POINT(16.348243 48.199678)"); - layer.add(tx, shortLineString); - layer.add(tx, longLineString); - tx.commit(); - } + /** + * This test case is designed to capture the conditions described in the bug + * report at https://github.com/neo4j/neo4j-spatial/issues/11. There are + * three geometries, one pPoint and two LineStrings, one short and one long. + * The short LineString is closer to the Point, but SearchClosest returns + * the long LineString. + */ + @Test + public void testSearchClosestWithShortLongLineStrings() throws ParseException { + String layerName = "test"; + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + Geometry shortLineString; + Geometry longLineString; + Geometry point; + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = spatial.getOrCreateEditableLayer(tx, layerName, "WKT"); + WKTReader wkt = new WKTReader(layer.getGeometryFactory()); + shortLineString = wkt.read("LINESTRING(16.3493032 48.199882,16.3479487 48.1997337)"); + longLineString = wkt.read( + "LINESTRING(16.3178388 48.1979135,16.3195494 48.1978011,16.3220815 48.197824,16.3259696 48.1978297,16.3281211 48.1975952,16.3312482 48.1968743,16.3327931 48.1965196,16.3354641 48.1959911,16.3384376 48.1959609,16.3395792 48.1960223,16.3458708 48.1970974,16.3477719 48.1975147,16.348008 48.1975665,16.3505572 48.1984533,16.3535613 48.1994545,16.3559474 48.2011765,16.3567056 48.2025723,16.3571261 48.2038308,16.3578393 48.205176)"); + point = wkt.read("POINT(16.348243 48.199678)"); + layer.add(tx, shortLineString); + layer.add(tx, longLineString); + tx.commit(); + } - // First calculate the distances explicitly - Geometry closestGeom = null; - double closestDistance = Double.MAX_VALUE; - System.out.println("Calculating explicit distance to the point " + point + ":"); - for (Geometry geom : new Geometry[]{shortLineString, longLineString}) { - double distance = point.distance(geom); - System.out.println("\tDistance " + distance + " to " + geom); - if (distance < closestDistance) { - closestDistance = distance; - closestGeom = geom; - } - } - assertNotNull(closestGeom, "Expected to find a clistestGeom"); - System.out.println("Found closest: " + closestGeom); - System.out.println(); + // First calculate the distances explicitly + Geometry closestGeom = null; + double closestDistance = Double.MAX_VALUE; + System.out.println("Calculating explicit distance to the point " + point + ":"); + for (Geometry geom : new Geometry[]{shortLineString, longLineString}) { + double distance = point.distance(geom); + System.out.println("\tDistance " + distance + " to " + geom); + if (distance < closestDistance) { + closestDistance = distance; + closestGeom = geom; + } + } + assertNotNull(closestGeom, "Expected to find a clistestGeom"); + System.out.println("Found closest: " + closestGeom); + System.out.println(); - // Now use the SearchClosest class to perform the search for the closest - System.out.println("Searching for geometries close to " + point); - GeoPipeline pipeline; - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), 100) - .sort("Distance") - .getMin("Distance"); - for (SpatialRecord result : pipeline) { - System.out.println("\tGot search result: " + result); - assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); - } - tx.commit(); - } + // Now use the SearchClosest class to perform the search for the closest + System.out.println("Searching for geometries close to " + point); + GeoPipeline pipeline; + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), 100) + .sort("Distance") + .getMin("Distance"); + for (SpatialRecord result : pipeline) { + System.out.println("\tGot search result: " + result); + assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); + } + tx.commit(); + } - // Repeat with an envelope - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - Envelope env = new Envelope(point.getCoordinate().x, point.getCoordinate().x, point.getCoordinate().y, point.getCoordinate().y); - env.expandToInclude(shortLineString.getEnvelopeInternal()); - env.expandToInclude(longLineString.getEnvelopeInternal()); - pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), env) - .sort("Distance") - .getMin("Distance"); - System.out.println("Searching for geometries close to " + point + " within " + env); - for (SpatialRecord result : pipeline) { - System.out.println("\tGot search result: " + result); - assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); - } - tx.commit(); - } + // Repeat with an envelope + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + Envelope env = new Envelope(point.getCoordinate().x, point.getCoordinate().x, point.getCoordinate().y, + point.getCoordinate().y); + env.expandToInclude(shortLineString.getEnvelopeInternal()); + env.expandToInclude(longLineString.getEnvelopeInternal()); + pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), env) + .sort("Distance") + .getMin("Distance"); + System.out.println("Searching for geometries close to " + point + " within " + env); + for (SpatialRecord result : pipeline) { + System.out.println("\tGot search result: " + result); + assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); + } + tx.commit(); + } - // Repeat with a buffer big enough to work - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - double buffer = 0.0001; - pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), buffer) - .sort("Distance") - .getMin("Distance"); - System.out.println("Searching for geometries close to " + point + " within buffer " + buffer); - for (SpatialRecord result : pipeline) { - System.out.println("\tGot search result: " + result); - assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); - } - tx.commit(); - } + // Repeat with a buffer big enough to work + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + double buffer = 0.0001; + pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), buffer) + .sort("Distance") + .getMin("Distance"); + System.out.println("Searching for geometries close to " + point + " within buffer " + buffer); + for (SpatialRecord result : pipeline) { + System.out.println("\tGot search result: " + result); + assertEquals(closestGeom.toString(), result.getGeometry().toString(), "Did not find the closest"); + } + tx.commit(); + } - // Repeat with a buffer too small to work correctly - //TODO: Since the new Envelope class in graph-collections seems to not have the same bug as the old JTS Envelope, this test case no longer works. We should think of a new test case. + // Repeat with a buffer too small to work correctly + //TODO: Since the new Envelope class in graph-collections seems to not have the same bug as the old JTS Envelope, this test case no longer works. We should think of a new test case. // buffer = 0.00001; // closest = new SearchClosest(point, buffer); // System.out.println("Searching for geometries close to " + point + " within buffer " + buffer); @@ -137,19 +140,22 @@ public void testSearchClosestWithShortLongLineStrings() throws ParseException { // assertThat("Unexpectedly found the closest", result.getGeometry().toString(), is(not(closestGeom.toString()))); // } - // Repeat with the new limit API - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - int limit = 10; - pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), limit) - .sort("Distance") - .getMin("Distance"); - System.out.println("Searching for geometries close to " + point + " within automatic window designed to get about " + limit + " geometries"); - for (SpatialRecord result : pipeline) { - System.out.println("\tGot search result: " + result); - MatcherAssert.assertThat("Did not find the closest", result.getGeometry().toString(), is(closestGeom.toString())); - } - tx.commit(); - } - } + // Repeat with the new limit API + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + int limit = 10; + pipeline = GeoPipeline.startNearestNeighborSearch(tx, layer, point.getCoordinate(), limit) + .sort("Distance") + .getMin("Distance"); + System.out.println( + "Searching for geometries close to " + point + " within automatic window designed to get about " + + limit + " geometries"); + for (SpatialRecord result : pipeline) { + System.out.println("\tGot search result: " + result); + MatcherAssert.assertThat("Did not find the closest", result.getGeometry().toString(), + is(closestGeom.toString())); + } + tx.commit(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestSpatialUtils.java b/src/test/java/org/neo4j/gis/spatial/TestSpatialUtils.java index 48f4f6c61..2ceb65594 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestSpatialUtils.java +++ b/src/test/java/org/neo4j/gis/spatial/TestSpatialUtils.java @@ -19,6 +19,11 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.charset.StandardCharsets; +import java.util.List; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; @@ -35,149 +40,150 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - public class TestSpatialUtils extends Neo4jTestCase { - @Test - public void testJTSLinearRef() { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - Geometry geometry; - try (Transaction tx = graphDb().beginTx()) { - EditableLayer layer = spatial.getOrCreateEditableLayer(tx, "jts"); - Coordinate[] coordinates = new Coordinate[]{new Coordinate(0, 0), new Coordinate(0, 1), new Coordinate(1, 1)}; - geometry = layer.getGeometryFactory().createLineString(coordinates); - layer.add(tx, geometry); - debugLRS(geometry); - tx.commit(); - } + @Test + public void testJTSLinearRef() { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + Geometry geometry; + try (Transaction tx = graphDb().beginTx()) { + EditableLayer layer = spatial.getOrCreateEditableLayer(tx, "jts"); + Coordinate[] coordinates = new Coordinate[]{new Coordinate(0, 0), new Coordinate(0, 1), + new Coordinate(1, 1)}; + geometry = layer.getGeometryFactory().createLineString(coordinates); + layer.add(tx, geometry); + debugLRS(geometry); + tx.commit(); + } - try(Transaction tx = graphDb().beginTx()) { - double delta = 0.0001; - Layer layer = spatial.getLayer(tx, "jts"); - // Now test the new API in the topology utils - Point point = SpatialTopologyUtils.locatePoint(layer, geometry, 1.5, 0.5); - assertEquals(0.5, point.getX(), delta, "X location incorrect"); - assertEquals(1.5, point.getY(), delta, "Y location incorrect"); - point = SpatialTopologyUtils.locatePoint(layer, geometry, 1.5, -0.5); - assertEquals(0.5, point.getX(), delta, "X location incorrect"); - assertEquals(0.5, point.getY(), delta, "Y location incorrect"); - point = SpatialTopologyUtils.locatePoint(layer, geometry, 0.5, 0.5); - assertEquals(-0.5, point.getX(), delta, "X location incorrect"); - assertEquals(0.5, point.getY(), delta, "Y location incorrect"); - point = SpatialTopologyUtils.locatePoint(layer, geometry, 0.5, -0.5); - assertEquals(0.5, point.getX(), delta, "X location incorrect"); - assertEquals(0.5, point.getY(), delta, "Y location incorrect"); - tx.commit(); - } - } + try (Transaction tx = graphDb().beginTx()) { + double delta = 0.0001; + Layer layer = spatial.getLayer(tx, "jts"); + // Now test the new API in the topology utils + Point point = SpatialTopologyUtils.locatePoint(layer, geometry, 1.5, 0.5); + assertEquals(0.5, point.getX(), delta, "X location incorrect"); + assertEquals(1.5, point.getY(), delta, "Y location incorrect"); + point = SpatialTopologyUtils.locatePoint(layer, geometry, 1.5, -0.5); + assertEquals(0.5, point.getX(), delta, "X location incorrect"); + assertEquals(0.5, point.getY(), delta, "Y location incorrect"); + point = SpatialTopologyUtils.locatePoint(layer, geometry, 0.5, 0.5); + assertEquals(-0.5, point.getX(), delta, "X location incorrect"); + assertEquals(0.5, point.getY(), delta, "Y location incorrect"); + point = SpatialTopologyUtils.locatePoint(layer, geometry, 0.5, -0.5); + assertEquals(0.5, point.getX(), delta, "X location incorrect"); + assertEquals(0.5, point.getY(), delta, "Y location incorrect"); + tx.commit(); + } + } - /** - * This method just prints a bunch of information to the console to help - * understand the behaviour of the JTS LRS methods better. Currently no - * assertions are made. - */ - private void debugLRS(Geometry geometry) { - LengthIndexedLine line = new org.locationtech.jts.linearref.LengthIndexedLine(geometry); - double length = line.getEndIndex() - line.getStartIndex(); - System.out.println("Have Geometry: " + geometry); - System.out.println("Have LengthIndexedLine: " + line); - System.out.println("Have start index: " + line.getStartIndex()); - System.out.println("Have end index: " + line.getEndIndex()); - System.out.println("Have length: " + length); - System.out.println("Extracting point at position 0.0: " + line.extractPoint(0.0)); - System.out.println("Extracting point at position 0.1: " + line.extractPoint(0.1)); - System.out.println("Extracting point at position 0.5: " + line.extractPoint(0.5)); - System.out.println("Extracting point at position 0.9: " + line.extractPoint(0.9)); - System.out.println("Extracting point at position 1.0: " + line.extractPoint(1.0)); - System.out.println("Extracting point at position 1.5: " + line.extractPoint(1.5)); - System.out.println("Extracting point at position 1.5 offset 0.5: " + line.extractPoint(1.5, 0.5)); - System.out.println("Extracting point at position 1.5 offset -0.5: " + line.extractPoint(1.5, -0.5)); - System.out.println("Extracting point at position " + length + ": " + line.extractPoint(length)); - System.out.println("Extracting point at position " + (length / 2) + ": " + line.extractPoint(length / 2)); - System.out.println("Extracting line from position 0.1 to 0.2: " + line.extractLine(0.1, 0.2)); - System.out.println("Extracting line from position 0.0 to " + (length / 2) + ": " + line.extractLine(0, length / 2)); - LocationIndexedLine pline = new LocationIndexedLine(geometry); - System.out.println("Have LocationIndexedLine: " + pline); - System.out.println("Have start index: " + pline.getStartIndex()); - System.out.println("Have end index: " + pline.getEndIndex()); - System.out.println("Extracting point at start: " + pline.extractPoint(pline.getStartIndex())); - System.out.println("Extracting point at end: " + pline.extractPoint(pline.getEndIndex())); - System.out.println("Extracting point at start offset 0.5: " + pline.extractPoint(pline.getStartIndex(), 0.5)); - System.out.println("Extracting point at end offset 0.5: " + pline.extractPoint(pline.getEndIndex(), 0.5)); - } + /** + * This method just prints a bunch of information to the console to help + * understand the behaviour of the JTS LRS methods better. Currently no + * assertions are made. + */ + private void debugLRS(Geometry geometry) { + LengthIndexedLine line = new org.locationtech.jts.linearref.LengthIndexedLine(geometry); + double length = line.getEndIndex() - line.getStartIndex(); + System.out.println("Have Geometry: " + geometry); + System.out.println("Have LengthIndexedLine: " + line); + System.out.println("Have start index: " + line.getStartIndex()); + System.out.println("Have end index: " + line.getEndIndex()); + System.out.println("Have length: " + length); + System.out.println("Extracting point at position 0.0: " + line.extractPoint(0.0)); + System.out.println("Extracting point at position 0.1: " + line.extractPoint(0.1)); + System.out.println("Extracting point at position 0.5: " + line.extractPoint(0.5)); + System.out.println("Extracting point at position 0.9: " + line.extractPoint(0.9)); + System.out.println("Extracting point at position 1.0: " + line.extractPoint(1.0)); + System.out.println("Extracting point at position 1.5: " + line.extractPoint(1.5)); + System.out.println("Extracting point at position 1.5 offset 0.5: " + line.extractPoint(1.5, 0.5)); + System.out.println("Extracting point at position 1.5 offset -0.5: " + line.extractPoint(1.5, -0.5)); + System.out.println("Extracting point at position " + length + ": " + line.extractPoint(length)); + System.out.println("Extracting point at position " + (length / 2) + ": " + line.extractPoint(length / 2)); + System.out.println("Extracting line from position 0.1 to 0.2: " + line.extractLine(0.1, 0.2)); + System.out.println( + "Extracting line from position 0.0 to " + (length / 2) + ": " + line.extractLine(0, length / 2)); + LocationIndexedLine pline = new LocationIndexedLine(geometry); + System.out.println("Have LocationIndexedLine: " + pline); + System.out.println("Have start index: " + pline.getStartIndex()); + System.out.println("Have end index: " + pline.getEndIndex()); + System.out.println("Extracting point at start: " + pline.extractPoint(pline.getStartIndex())); + System.out.println("Extracting point at end: " + pline.extractPoint(pline.getEndIndex())); + System.out.println("Extracting point at start offset 0.5: " + pline.extractPoint(pline.getStartIndex(), 0.5)); + System.out.println("Extracting point at end offset 0.5: " + pline.extractPoint(pline.getEndIndex(), 0.5)); + } - @Test - public void testSnapping() throws Exception { - // This was an ignored test, so perhaps not worth saving? - printDatabaseStats(); - String osm = "map.osm"; - loadTestOsmData(osm, 1000); - printDatabaseStats(); + @Test + public void testSnapping() throws Exception { + // This was an ignored test, so perhaps not worth saving? + printDatabaseStats(); + String osm = "map.osm"; + loadTestOsmData(osm, 1000); + printDatabaseStats(); - // Define dynamic layers - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try(Transaction tx = graphDb().beginTx()) { - OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osm); - osmLayer.addSimpleDynamicLayer(tx, "highway", "primary"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "secondary"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "tertiary"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "residential"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "footway"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "cycleway"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "track"); - osmLayer.addSimpleDynamicLayer(tx, "highway", "path"); - osmLayer.addSimpleDynamicLayer(tx, "railway", null); - osmLayer.addSimpleDynamicLayer(tx, "highway", null); - tx.commit(); - } + // Define dynamic layers + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osm); + osmLayer.addSimpleDynamicLayer(tx, "highway", "primary"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "secondary"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "tertiary"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "residential"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "footway"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "cycleway"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "track"); + osmLayer.addSimpleDynamicLayer(tx, "highway", "path"); + osmLayer.addSimpleDynamicLayer(tx, "railway", null); + osmLayer.addSimpleDynamicLayer(tx, "highway", null); + tx.commit(); + } - // Now test snapping to a layer - try(Transaction tx = graphDb().beginTx()) { - OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osm); - OSMDataset.fromLayer(tx, osmLayer); // cache for future usage below - GeometryFactory factory = osmLayer.getGeometryFactory(); - EditableLayerImpl resultsLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "testSnapping_results"); - String[] fieldsNames = new String[]{"snap-id", "description", "distance"}; - resultsLayer.setExtraPropertyNames(fieldsNames, tx); - Point point = factory.createPoint(new Coordinate(12.9777, 56.0555)); - resultsLayer.add(tx, point, fieldsNames, new Object[]{0L, "Point to snap", 0L}); - for (String layerName : new String[]{"railway", "highway-residential"}) { - Layer layer = osmLayer.getLayer(tx, layerName); - assertNotNull(layer, "Missing layer: " + layerName); - System.out.println("Closest features in " + layerName + " to point " + point + ":"); - List edgeResults = SpatialTopologyUtils.findClosestEdges(tx, point, layer); - for (PointResult result : edgeResults) { - System.out.println("\t" + result); - resultsLayer.add(tx, result.getKey(), fieldsNames, new Object[]{result.getValue().getGeomNode().getId(), - "Snapped point to layer " + layerName + ": " + result.getValue().getGeometry().toString(), - (long) (1000000 * result.getDistance())}); - } - if (edgeResults.size() > 0) { - PointResult closest = edgeResults.get(0); - Point closestPoint = closest.getKey(); + // Now test snapping to a layer + try (Transaction tx = graphDb().beginTx()) { + OSMLayer osmLayer = (OSMLayer) spatial.getLayer(tx, osm); + OSMDataset.fromLayer(tx, osmLayer); // cache for future usage below + GeometryFactory factory = osmLayer.getGeometryFactory(); + EditableLayerImpl resultsLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, + "testSnapping_results"); + String[] fieldsNames = new String[]{"snap-id", "description", "distance"}; + resultsLayer.setExtraPropertyNames(fieldsNames, tx); + Point point = factory.createPoint(new Coordinate(12.9777, 56.0555)); + resultsLayer.add(tx, point, fieldsNames, new Object[]{0L, "Point to snap", 0L}); + for (String layerName : new String[]{"railway", "highway-residential"}) { + Layer layer = osmLayer.getLayer(tx, layerName); + assertNotNull(layer, "Missing layer: " + layerName); + System.out.println("Closest features in " + layerName + " to point " + point + ":"); + List edgeResults = SpatialTopologyUtils.findClosestEdges(tx, point, layer); + for (PointResult result : edgeResults) { + System.out.println("\t" + result); + resultsLayer.add(tx, result.getKey(), fieldsNames, + new Object[]{result.getValue().getGeomNode().getId(), + "Snapped point to layer " + layerName + ": " + result.getValue().getGeometry() + .toString(), + (long) (1000000 * result.getDistance())}); + } + if (edgeResults.size() > 0) { + PointResult closest = edgeResults.get(0); + Point closestPoint = closest.getKey(); - SpatialDatabaseRecord wayRecord = closest.getValue(); - OSMDataset.Way way = ((OSMDataset) osmLayer.getDataset()).getWayFrom(wayRecord.getGeomNode()); - OSMDataset.WayPoint wayPoint = way.getPointAt(closestPoint.getCoordinate()); - // TODO: presumably we meant to assert something here? - } - } - tx.commit(); - } + SpatialDatabaseRecord wayRecord = closest.getValue(); + OSMDataset.Way way = ((OSMDataset) osmLayer.getDataset()).getWayFrom(wayRecord.getGeomNode()); + OSMDataset.WayPoint wayPoint = way.getPointAt(closestPoint.getCoordinate()); + // TODO: presumably we meant to assert something here? + } + } + tx.commit(); + } - } + } - @SuppressWarnings("SameParameterValue") - private void loadTestOsmData(String layerName, int commitInterval) throws Exception { - System.out.println("\n=== Loading layer " + layerName + " from " + layerName + " ==="); - OSMImporter importer = new OSMImporter(layerName); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(graphDb(), layerName, commitInterval); - importer.reIndex(graphDb(), commitInterval); - } + @SuppressWarnings("SameParameterValue") + private void loadTestOsmData(String layerName, int commitInterval) throws Exception { + System.out.println("\n=== Loading layer " + layerName + " from " + layerName + " ==="); + OSMImporter importer = new OSMImporter(layerName); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(graphDb(), layerName, commitInterval); + importer.reIndex(graphDb(), commitInterval); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TestsForDocs.java b/src/test/java/org/neo4j/gis/spatial/TestsForDocs.java index bd86e301c..8efdfd89b 100644 --- a/src/test/java/org/neo4j/gis/spatial/TestsForDocs.java +++ b/src/test/java/org/neo4j/gis/spatial/TestsForDocs.java @@ -19,6 +19,15 @@ */ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import org.geotools.api.data.DataStore; import org.geotools.data.neo4j.Neo4jSpatialDataStore; import org.geotools.data.simple.SimpleFeatureCollection; @@ -44,16 +53,6 @@ import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - /** * Some test code written specifically for the user manual. This normally means * we repeat some of the setup/teardown code in each test so that it is a more @@ -62,181 +61,190 @@ * users own coding experience. */ public class TestsForDocs { - private DatabaseManagementService databases; - private GraphDatabaseService graphDb; - - @BeforeEach - public void setUp() throws Exception { - this.databases = new DatabaseManagementServiceBuilder(Path.of("target", "docs-db")).build(); - this.graphDb = databases.database(DEFAULT_DATABASE_NAME); - try (Transaction tx = this.graphDb.beginTx()) { - tx.getAllRelationships().forEach(Relationship::delete); - tx.commit(); - } - try (Transaction tx = this.graphDb.beginTx()) { - tx.getAllNodes().forEach(Node::delete); - tx.commit(); - } - } - - @AfterEach - public void tearDown() { - this.databases.shutdown(); - this.databases = null; - this.graphDb = null; - } - - private void checkIndexAndFeatureCount(String layerName) throws IOException { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb.beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - if (layer.getIndex().count(tx) < 1) { - System.out.println("Warning: index count zero: " + layer.getName()); - } - System.out.println("Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); - tx.commit(); - } - DataStore store = new Neo4jSpatialDataStore(graphDb); - SimpleFeatureCollection features = store.getFeatureSource(layerName).getFeatures(); - System.out.println("Layer '" + layerName + "' has " + features.size() + " features"); - try (Transaction tx = graphDb.beginTx()) { - Layer layer = spatial.getLayer(tx, layerName); - assertEquals(layer.getIndex().count(tx), features.size(), "FeatureCollection.size for layer '" + layer.getName() + "' not the same as index count"); - if (layer instanceof OSMLayer) - checkOSMAPI(tx, (OSMLayer) layer); - tx.commit(); - } - } - - private void checkOSMAPI(Transaction tx, OSMLayer layer) { - HashMap waysFound = new HashMap<>(); - String mostCommon = null; - int mostCount = 0; - OSMDataset osm = OSMDataset.fromLayer(tx, layer); - Node wayNode = osm.getAllWayNodes(tx).iterator().next(); - Way way = osm.getWayFrom(wayNode); - System.out.println("Got first way " + way); - for (WayPoint n : way.getWayPoints()) { - Way w = n.getWay(); - String wayId = w.getNode().getElementId(); - if (!waysFound.containsKey(wayId)) { - waysFound.put(wayId, 0); - } - waysFound.put(wayId, waysFound.get(wayId) + 1); - if (waysFound.get(wayId) > mostCount) { - mostCommon = wayId; - mostCount = waysFound.get(wayId); - } - } - System.out.println("Found " + waysFound.size() + " ways overlapping '" + way + "'"); - for (String wayId : waysFound.keySet()) { - System.out.println("\t" + wayId + ":\t" + waysFound.get(wayId) + (wayId.equals(way.getNode().getElementId()) ? "\t(original way)" : "")); - } - assertTrue(way.equals(osm.getWayFromId(tx, mostCommon)), "Start way should be most found way"); - } - - private void importMapOSM(GraphDatabaseService db) throws Exception { - // START SNIPPET: importOsm tag::importOsm[] - OSMImporter importer = new OSMImporter("map.osm"); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(db, "map.osm"); - importer.reIndex(db); - // END SNIPPET: importOsm end::importOsm[] - } - - /** - * Sample code for importing Open Street Map example. - */ - @Test - public void testImportOSM() throws Exception { - System.out.println("\n=== Simple test map.osm ==="); - importMapOSM(graphDb); - GraphDatabaseService database = graphDb; - // START SNIPPET: searchBBox tag::searchBBox[] - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - try (Transaction tx = database.beginTx()) { - Layer layer = spatial.getLayer(tx, "map.osm"); - LayerIndexReader spatialIndex = layer.getIndex(); - System.out.println("Have " + spatialIndex.count(tx) + " geometries in " + spatialIndex.getBoundingBox(tx)); - - Envelope bbox = new Envelope(12.94, 12.96, 56.04, 56.06); - List results = GeoPipeline - .startIntersectWindowSearch(tx, layer, bbox) - .toSpatialDatabaseRecordList(); - - doGeometryTestsOnResults(bbox, results); - tx.commit(); - } - // END SNIPPET: searchBBox end::searchBBox[] - - checkIndexAndFeatureCount("map.osm"); - } - - @Test - public void testImportShapefile() throws Exception { - System.out.println("\n=== Test Import Shapefile ==="); - GraphDatabaseService database = graphDb; - - // START SNIPPET: importShapefile tag::importShapefile[] - ShapefileImporter importer = new ShapefileImporter(database); - importer.importFile("shp/highway.shp", "highway", StandardCharsets.UTF_8); - // END SNIPPET: importShapefile end::importShapefile[] - - checkIndexAndFeatureCount("highway"); - } - - @Test - public void testExportShapefileFromOSM() throws Exception { - System.out.println("\n=== Test import map.osm, create DynamicLayer and export shapefile ==="); - importMapOSM(graphDb); - GraphDatabaseService database = graphDb; - // START SNIPPET: exportShapefileFromOSM tag::exportShapefileFromOSM[] - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - String wayLayerName; - try (Transaction tx = database.beginTx()) { - OSMLayer layer = (OSMLayer) spatial.getLayer(tx, "map.osm"); - DynamicLayerConfig wayLayer = layer.addSimpleDynamicLayer(tx, Constants.GTYPE_LINESTRING); - wayLayerName = wayLayer.getName(); - tx.commit(); - } - ShapefileExporter shpExporter = new ShapefileExporter(database); - shpExporter.exportLayer(wayLayerName); - // END SNIPPET: exportShapefileFromOSM end::exportShapefileFromOSM[] - } - - @Test - public void testExportShapefileFromQuery() throws Exception { - System.out.println("\n=== Test import map.osm, create DynamicLayer and export shapefile ==="); - importMapOSM(graphDb); - GraphDatabaseService database = graphDb; - // START SNIPPET: exportShapefileFromQuery tag::exportShapefileFromQuery[] - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); - Envelope bbox = new Envelope(12.94, 12.96, 56.04, 56.06); - List results; - try (Transaction tx = database.beginTx()) { - Layer layer = spatial.getLayer(tx, "map.osm"); - LayerIndexReader spatialIndex = layer.getIndex(); - System.out.println("Have " + spatialIndex.count(tx) + " geometries in " + spatialIndex.getBoundingBox(tx)); - - results = GeoPipeline - .startIntersectWindowSearch(tx, layer, bbox) - .toSpatialDatabaseRecordList(); - - spatial.createResultsLayer(tx, "results", results); - tx.commit(); - - } - ShapefileExporter shpExporter = new ShapefileExporter(database); - shpExporter.exportLayer("results"); - // END SNIPPET: exportShapefileFromQuery end::exportShapefileFromQuery[] - doGeometryTestsOnResults(bbox, results); - } - - private void doGeometryTestsOnResults(Envelope bbox, List results) { - System.out.println("Found " + results.size() + " geometries in " + bbox); - Geometry geometry = results.get(0).getGeometry(); - System.out.println("First geometry is " + geometry); - geometry.buffer(2); - } + + private DatabaseManagementService databases; + private GraphDatabaseService graphDb; + + @BeforeEach + public void setUp() throws Exception { + this.databases = new DatabaseManagementServiceBuilder(Path.of("target", "docs-db")).build(); + this.graphDb = databases.database(DEFAULT_DATABASE_NAME); + try (Transaction tx = this.graphDb.beginTx()) { + tx.getAllRelationships().forEach(Relationship::delete); + tx.commit(); + } + try (Transaction tx = this.graphDb.beginTx()) { + tx.getAllNodes().forEach(Node::delete); + tx.commit(); + } + } + + @AfterEach + public void tearDown() { + this.databases.shutdown(); + this.databases = null; + this.graphDb = null; + } + + private void checkIndexAndFeatureCount(String layerName) throws IOException { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb.beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + if (layer.getIndex().count(tx) < 1) { + System.out.println("Warning: index count zero: " + layer.getName()); + } + System.out.println( + "Layer '" + layer.getName() + "' has " + layer.getIndex().count(tx) + " entries in the index"); + tx.commit(); + } + DataStore store = new Neo4jSpatialDataStore(graphDb); + SimpleFeatureCollection features = store.getFeatureSource(layerName).getFeatures(); + System.out.println("Layer '" + layerName + "' has " + features.size() + " features"); + try (Transaction tx = graphDb.beginTx()) { + Layer layer = spatial.getLayer(tx, layerName); + assertEquals(layer.getIndex().count(tx), features.size(), + "FeatureCollection.size for layer '" + layer.getName() + "' not the same as index count"); + if (layer instanceof OSMLayer) { + checkOSMAPI(tx, (OSMLayer) layer); + } + tx.commit(); + } + } + + private void checkOSMAPI(Transaction tx, OSMLayer layer) { + HashMap waysFound = new HashMap<>(); + String mostCommon = null; + int mostCount = 0; + OSMDataset osm = OSMDataset.fromLayer(tx, layer); + Node wayNode = osm.getAllWayNodes(tx).iterator().next(); + Way way = osm.getWayFrom(wayNode); + System.out.println("Got first way " + way); + for (WayPoint n : way.getWayPoints()) { + Way w = n.getWay(); + String wayId = w.getNode().getElementId(); + if (!waysFound.containsKey(wayId)) { + waysFound.put(wayId, 0); + } + waysFound.put(wayId, waysFound.get(wayId) + 1); + if (waysFound.get(wayId) > mostCount) { + mostCommon = wayId; + mostCount = waysFound.get(wayId); + } + } + System.out.println("Found " + waysFound.size() + " ways overlapping '" + way + "'"); + for (String wayId : waysFound.keySet()) { + System.out.println("\t" + wayId + ":\t" + waysFound.get(wayId) + (wayId.equals(way.getNode().getElementId()) + ? "\t(original way)" : "")); + } + assertTrue(way.equals(osm.getWayFromId(tx, mostCommon)), "Start way should be most found way"); + } + + private void importMapOSM(GraphDatabaseService db) throws Exception { + // START SNIPPET: importOsm tag::importOsm[] + OSMImporter importer = new OSMImporter("map.osm"); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(db, "map.osm"); + importer.reIndex(db); + // END SNIPPET: importOsm end::importOsm[] + } + + /** + * Sample code for importing Open Street Map example. + */ + @Test + public void testImportOSM() throws Exception { + System.out.println("\n=== Simple test map.osm ==="); + importMapOSM(graphDb); + GraphDatabaseService database = graphDb; + // START SNIPPET: searchBBox tag::searchBBox[] + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + try (Transaction tx = database.beginTx()) { + Layer layer = spatial.getLayer(tx, "map.osm"); + LayerIndexReader spatialIndex = layer.getIndex(); + System.out.println("Have " + spatialIndex.count(tx) + " geometries in " + spatialIndex.getBoundingBox(tx)); + + Envelope bbox = new Envelope(12.94, 12.96, 56.04, 56.06); + List results = GeoPipeline + .startIntersectWindowSearch(tx, layer, bbox) + .toSpatialDatabaseRecordList(); + + doGeometryTestsOnResults(bbox, results); + tx.commit(); + } + // END SNIPPET: searchBBox end::searchBBox[] + + checkIndexAndFeatureCount("map.osm"); + } + + @Test + public void testImportShapefile() throws Exception { + System.out.println("\n=== Test Import Shapefile ==="); + GraphDatabaseService database = graphDb; + + // START SNIPPET: importShapefile tag::importShapefile[] + ShapefileImporter importer = new ShapefileImporter(database); + importer.importFile("shp/highway.shp", "highway", StandardCharsets.UTF_8); + // END SNIPPET: importShapefile end::importShapefile[] + + checkIndexAndFeatureCount("highway"); + } + + @Test + public void testExportShapefileFromOSM() throws Exception { + System.out.println("\n=== Test import map.osm, create DynamicLayer and export shapefile ==="); + importMapOSM(graphDb); + GraphDatabaseService database = graphDb; + // START SNIPPET: exportShapefileFromOSM tag::exportShapefileFromOSM[] + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + String wayLayerName; + try (Transaction tx = database.beginTx()) { + OSMLayer layer = (OSMLayer) spatial.getLayer(tx, "map.osm"); + DynamicLayerConfig wayLayer = layer.addSimpleDynamicLayer(tx, Constants.GTYPE_LINESTRING); + wayLayerName = wayLayer.getName(); + tx.commit(); + } + ShapefileExporter shpExporter = new ShapefileExporter(database); + shpExporter.exportLayer(wayLayerName); + // END SNIPPET: exportShapefileFromOSM end::exportShapefileFromOSM[] + } + + @Test + public void testExportShapefileFromQuery() throws Exception { + System.out.println("\n=== Test import map.osm, create DynamicLayer and export shapefile ==="); + importMapOSM(graphDb); + GraphDatabaseService database = graphDb; + // START SNIPPET: exportShapefileFromQuery tag::exportShapefileFromQuery[] + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb, SecurityContext.AUTH_DISABLED)); + Envelope bbox = new Envelope(12.94, 12.96, 56.04, 56.06); + List results; + try (Transaction tx = database.beginTx()) { + Layer layer = spatial.getLayer(tx, "map.osm"); + LayerIndexReader spatialIndex = layer.getIndex(); + System.out.println("Have " + spatialIndex.count(tx) + " geometries in " + spatialIndex.getBoundingBox(tx)); + + results = GeoPipeline + .startIntersectWindowSearch(tx, layer, bbox) + .toSpatialDatabaseRecordList(); + + spatial.createResultsLayer(tx, "results", results); + tx.commit(); + + } + ShapefileExporter shpExporter = new ShapefileExporter(database); + shpExporter.exportLayer("results"); + // END SNIPPET: exportShapefileFromQuery end::exportShapefileFromQuery[] + doGeometryTestsOnResults(bbox, results); + } + + private void doGeometryTestsOnResults(Envelope bbox, List results) { + System.out.println("Found " + results.size() + " geometries in " + bbox); + Geometry geometry = results.get(0).getGeometry(); + System.out.println("First geometry is " + geometry); + geometry.buffer(2); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/TryWithResourceTest.java b/src/test/java/org/neo4j/gis/spatial/TryWithResourceTest.java index 14cec27ea..81dc15dd9 100644 --- a/src/test/java/org/neo4j/gis/spatial/TryWithResourceTest.java +++ b/src/test/java/org/neo4j/gis/spatial/TryWithResourceTest.java @@ -1,5 +1,9 @@ package org.neo4j.gis.spatial; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; import org.junit.jupiter.api.Test; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.graphdb.GraphDatabaseService; @@ -7,52 +11,53 @@ import org.neo4j.graphdb.Transaction; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - @SuppressWarnings("ConstantConditions") public class TryWithResourceTest { - public static final String MESSAGE = "I want to see this"; + public static final String MESSAGE = "I want to see this"; - @Test - public void testSuppressedException() { - try { - DatabaseManagementService databases = new TestDatabaseManagementServiceBuilder(new File("target/resource").toPath()).impermanent().build(); - GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); - try (Transaction tx = db.beginTx()) { - Node n = tx.createNode(); - try (Transaction tx2 = db.beginTx()) { - n.setProperty("foo", "bar"); - if (true) throw new Exception(MESSAGE); - tx2.commit(); - } - tx.commit(); - } finally { - databases.shutdown(); - } - } catch (Exception e) { - assertEquals(MESSAGE, e.getMessage()); - } - } + @Test + public void testSuppressedException() { + try { + DatabaseManagementService databases = new TestDatabaseManagementServiceBuilder( + new File("target/resource").toPath()).impermanent().build(); + GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); + try (Transaction tx = db.beginTx()) { + Node n = tx.createNode(); + try (Transaction tx2 = db.beginTx()) { + n.setProperty("foo", "bar"); + if (true) { + throw new Exception(MESSAGE); + } + tx2.commit(); + } + tx.commit(); + } finally { + databases.shutdown(); + } + } catch (Exception e) { + assertEquals(MESSAGE, e.getMessage()); + } + } - @Test - public void testSuppressedExceptionTopLevel() { - try { - DatabaseManagementService databases = new TestDatabaseManagementServiceBuilder(new File("target/resource").toPath()).impermanent().build(); - GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); - try (Transaction tx = db.beginTx()) { - Node n = tx.createNode(); - n.setProperty("foo", "bar"); - if (true) throw new Exception(MESSAGE); - tx.commit(); - } finally { - databases.shutdown(); - } - } catch (Exception e) { - assertEquals(MESSAGE, e.getMessage()); - } - } + @Test + public void testSuppressedExceptionTopLevel() { + try { + DatabaseManagementService databases = new TestDatabaseManagementServiceBuilder( + new File("target/resource").toPath()).impermanent().build(); + GraphDatabaseService db = databases.database(DEFAULT_DATABASE_NAME); + try (Transaction tx = db.beginTx()) { + Node n = tx.createNode(); + n.setProperty("foo", "bar"); + if (true) { + throw new Exception(MESSAGE); + } + tx.commit(); + } finally { + databases.shutdown(); + } + } catch (Exception e) { + assertEquals(MESSAGE, e.getMessage()); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashNativePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashNativePointIndexTest.java index b80ff28bf..218f05b0b 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashNativePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashNativePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerGeohashNativePointIndexTest extends NativePointIndexTestBase { - protected Class getIndexClass() { - return LayerGeohashPointIndex.class; - } + protected Class getIndexClass() { + return LayerGeohashPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerGeohashPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerGeohashPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashSimplePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashSimplePointIndexTest.java index bb4817a11..b42a49bcc 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashSimplePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerGeohashSimplePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerGeohashSimplePointIndexTest extends SimplePointIndexTestBase { - protected Class getIndexClass() { - return LayerGeohashPointIndex.class; - } + protected Class getIndexClass() { + return LayerGeohashPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerGeohashPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerGeohashPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertNativePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertNativePointIndexTest.java index bf36e08e7..edad64b56 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertNativePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertNativePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerHilbertNativePointIndexTest extends NativePointIndexTestBase { - protected Class getIndexClass() { - return LayerHilbertPointIndex.class; - } + protected Class getIndexClass() { + return LayerHilbertPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerHilbertPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerHilbertPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertSimplePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertSimplePointIndexTest.java index 360af8570..63fa502e6 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertSimplePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerHilbertSimplePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerHilbertSimplePointIndexTest extends SimplePointIndexTestBase { - protected Class getIndexClass() { - return LayerHilbertPointIndex.class; - } + protected Class getIndexClass() { + return LayerHilbertPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerHilbertPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerHilbertPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerIndexTestBase.java b/src/test/java/org/neo4j/gis/spatial/index/LayerIndexTestBase.java index 4389b9b42..92cca7045 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerIndexTestBase.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerIndexTestBase.java @@ -19,14 +19,41 @@ */ package org.neo4j.gis.spatial.index; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.geotools.referencing.crs.DefaultGeographicCRS; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.locationtech.jts.geom.*; -import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.mockito.Mockito; import org.neo4j.dbms.api.DatabaseManagementService; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.GeometryEncoder; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SimplePointLayer; +import org.neo4j.gis.spatial.SpatialDatabaseRecord; +import org.neo4j.gis.spatial.SpatialDatabaseService; import org.neo4j.gis.spatial.filter.SearchIntersect; import org.neo4j.gis.spatial.filter.SearchIntersectWindow; import org.neo4j.gis.spatial.pipes.GeoPipeFlow; @@ -40,225 +67,217 @@ import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - public abstract class LayerIndexTestBase { - protected DatabaseManagementService databases; - protected GraphDatabaseService graph; - protected SpatialDatabaseService spatial; - protected GeometryFactory geometryFactory = new GeometryFactory(); - protected GeometryEncoder encoder = makeGeometryEncoder(); + protected DatabaseManagementService databases; + protected GraphDatabaseService graph; + protected SpatialDatabaseService spatial; + protected GeometryFactory geometryFactory = new GeometryFactory(); + protected GeometryEncoder encoder = makeGeometryEncoder(); - protected abstract Class getIndexClass(); + protected abstract Class getIndexClass(); - protected abstract Class getEncoderClass(); + protected abstract Class getEncoderClass(); - protected abstract LayerIndexReader makeIndex(); + protected abstract LayerIndexReader makeIndex(); - protected abstract GeometryEncoder makeGeometryEncoder(); + protected abstract GeometryEncoder makeGeometryEncoder(); - protected SpatialIndexWriter mockLayerIndex() { - Layer layer = mockLayer(); - LayerIndexReader index = makeIndex(); - try (Transaction tx = graph.beginTx()) { - index.init(tx, spatial.indexManager, layer); - tx.commit(); - } - when(layer.getIndex()).thenReturn(index); - return (SpatialIndexWriter) index; - } + protected SpatialIndexWriter mockLayerIndex() { + Layer layer = mockLayer(); + LayerIndexReader index = makeIndex(); + try (Transaction tx = graph.beginTx()) { + index.init(tx, spatial.indexManager, layer); + tx.commit(); + } + when(layer.getIndex()).thenReturn(index); + return (SpatialIndexWriter) index; + } - protected Layer mockLayer() { - Node layerNode; - try (Transaction tx = graph.beginTx()) { - layerNode = tx.createNode(); - tx.commit(); - } - Layer layer = mock(Layer.class); - when(layer.getName()).thenReturn("test"); - when(layer.getGeometryEncoder()).thenReturn(encoder); - when(layer.getLayerNode(Mockito.any(Transaction.class))).thenReturn(layerNode); - when(layer.getGeometryFactory()).thenReturn(geometryFactory); - when(layer.getCoordinateReferenceSystem(Mockito.any(Transaction.class))).thenReturn(DefaultGeographicCRS.WGS84); - return layer; - } + protected Layer mockLayer() { + Node layerNode; + try (Transaction tx = graph.beginTx()) { + layerNode = tx.createNode(); + tx.commit(); + } + Layer layer = mock(Layer.class); + when(layer.getName()).thenReturn("test"); + when(layer.getGeometryEncoder()).thenReturn(encoder); + when(layer.getLayerNode(Mockito.any(Transaction.class))).thenReturn(layerNode); + when(layer.getGeometryFactory()).thenReturn(geometryFactory); + when(layer.getCoordinateReferenceSystem(Mockito.any(Transaction.class))).thenReturn(DefaultGeographicCRS.WGS84); + return layer; + } - protected void addSimplePoint(SpatialIndexWriter index, double x, double y) { - try (Transaction tx = graph.beginTx()) { - Node geomNode = tx.createNode(); - Point point = geometryFactory.createPoint(new Coordinate(x, y)); - encoder.encodeGeometry(tx, point, geomNode); - index.add(tx, geomNode); - tx.commit(); - } - } + protected void addSimplePoint(SpatialIndexWriter index, double x, double y) { + try (Transaction tx = graph.beginTx()) { + Node geomNode = tx.createNode(); + Point point = geometryFactory.createPoint(new Coordinate(x, y)); + encoder.encodeGeometry(tx, point, geomNode); + index.add(tx, geomNode); + tx.commit(); + } + } - @BeforeEach - public void setup() throws IOException { - File baseDir = new File("target/layers"); - FileUtils.deleteDirectory(baseDir.toPath()); - databases = new TestDatabaseManagementServiceBuilder(baseDir.toPath()).impermanent().build(); - graph = databases.database(DEFAULT_DATABASE_NAME); - spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graph, SecurityContext.AUTH_DISABLED)); - } + @BeforeEach + public void setup() throws IOException { + File baseDir = new File("target/layers"); + FileUtils.deleteDirectory(baseDir.toPath()); + databases = new TestDatabaseManagementServiceBuilder(baseDir.toPath()).impermanent().build(); + graph = databases.database(DEFAULT_DATABASE_NAME); + spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graph, SecurityContext.AUTH_DISABLED)); + } - @AfterEach - public void tearDown() { - if (graph != null) { - databases.shutdown(); - databases = null; - graph = null; - spatial = null; - } - } + @AfterEach + public void tearDown() { + if (graph != null) { + databases.shutdown(); + databases = null; + graph = null; + spatial = null; + } + } - @Test - public void shouldCreateAndFindIndexViaLayer() { - SimplePointLayer layer = makeTestPointLayer(); - LayerIndexReader index = layer.getIndex(); - try (Transaction tx = graph.beginTx()) { - assertThat("Should find the same index", index.getLayer().getName(), equalTo(spatial.getLayer(tx, "test").getName())); - assertThat("Index should be of right type", spatial.getLayer(tx, "test").getIndex().getClass(), equalTo(getIndexClass())); - } - } + @Test + public void shouldCreateAndFindIndexViaLayer() { + SimplePointLayer layer = makeTestPointLayer(); + LayerIndexReader index = layer.getIndex(); + try (Transaction tx = graph.beginTx()) { + assertThat("Should find the same index", index.getLayer().getName(), + equalTo(spatial.getLayer(tx, "test").getName())); + assertThat("Index should be of right type", spatial.getLayer(tx, "test").getIndex().getClass(), + equalTo(getIndexClass())); + } + } - @Test - public void shouldCreateAndFindAndDeleteIndexViaLayer() { - Layer layer = makeTestPointLayer(); - LayerIndexReader index = layer.getIndex(); - try (Transaction tx = graph.beginTx()) { - assertThat("Should find the same index", index.getLayer().getName(), equalTo(spatial.getLayer(tx, "test").getName())); - assertThat("Index should be of right type", spatial.getLayer(tx, "test").getIndex().getClass(), equalTo(getIndexClass())); - } - try (Transaction tx = graph.beginTx()) { - layer.delete(tx, new NullListener()); - layer = spatial.getLayer(tx, "test"); - tx.commit(); - } - assertThat("Expected no layer to be found", layer, is(nullValue())); - } + @Test + public void shouldCreateAndFindAndDeleteIndexViaLayer() { + Layer layer = makeTestPointLayer(); + LayerIndexReader index = layer.getIndex(); + try (Transaction tx = graph.beginTx()) { + assertThat("Should find the same index", index.getLayer().getName(), + equalTo(spatial.getLayer(tx, "test").getName())); + assertThat("Index should be of right type", spatial.getLayer(tx, "test").getIndex().getClass(), + equalTo(getIndexClass())); + } + try (Transaction tx = graph.beginTx()) { + layer.delete(tx, new NullListener()); + layer = spatial.getLayer(tx, "test"); + tx.commit(); + } + assertThat("Expected no layer to be found", layer, is(nullValue())); + } - private SimplePointLayer makeTestPointLayer() { - try (Transaction tx = graph.beginTx()) { - SimplePointLayer layer = spatial.createPointLayer(tx, "test", getIndexClass(), getEncoderClass()); - tx.commit(); - return layer; - } - } + private SimplePointLayer makeTestPointLayer() { + try (Transaction tx = graph.beginTx()) { + SimplePointLayer layer = spatial.createPointLayer(tx, "test", getIndexClass(), getEncoderClass()); + tx.commit(); + return layer; + } + } - @Test - public void shouldFindNodeAddedToIndexViaLayer() { - SimplePointLayer layer = makeTestPointLayer(); - SpatialDatabaseRecord added; - try (Transaction tx = graph.beginTx()) { - added = layer.add(tx, 1.0, 1.0); - tx.commit(); - } - try (Transaction tx = graph.beginTx()) { - List found = layer.findClosestPointsTo(tx, new Coordinate(1.0, 1.0), 0.5); - assertThat("Should find one geometry node", found.size(), equalTo(1)); - assertThat("Should find same geometry node", added.getGeomNode(), equalTo(found.get(0).getGeomNode())); - tx.commit(); - } - } + @Test + public void shouldFindNodeAddedToIndexViaLayer() { + SimplePointLayer layer = makeTestPointLayer(); + SpatialDatabaseRecord added; + try (Transaction tx = graph.beginTx()) { + added = layer.add(tx, 1.0, 1.0); + tx.commit(); + } + try (Transaction tx = graph.beginTx()) { + List found = layer.findClosestPointsTo(tx, new Coordinate(1.0, 1.0), 0.5); + assertThat("Should find one geometry node", found.size(), equalTo(1)); + assertThat("Should find same geometry node", added.getGeomNode(), equalTo(found.get(0).getGeomNode())); + tx.commit(); + } + } - @Test - public void shouldFindNodeAddedDirectlyToIndex() { - SpatialIndexWriter index = mockLayerIndex(); - addSimplePoint(index, 1.0, 1.0); - try (Transaction tx = graph.beginTx()) { - SearchResults results = index.searchIndex(tx, new SearchIntersectWindow(((LayerIndexReader) index).getLayer(), new Envelope(0.0, 2.0, 0.0, 2.0))); - List nodes = StreamSupport.stream(results.spliterator(), false).collect(Collectors.toList()); - assertThat("Index should contain one result", nodes.size(), equalTo(1)); - assertThat("Should find correct Geometry", encoder.decodeGeometry(nodes.get(0)), equalTo(geometryFactory.createPoint(new Coordinate(1.0, 1.0)))); - tx.commit(); - } - } + @Test + public void shouldFindNodeAddedDirectlyToIndex() { + SpatialIndexWriter index = mockLayerIndex(); + addSimplePoint(index, 1.0, 1.0); + try (Transaction tx = graph.beginTx()) { + SearchResults results = index.searchIndex(tx, + new SearchIntersectWindow(((LayerIndexReader) index).getLayer(), new Envelope(0.0, 2.0, 0.0, 2.0))); + List nodes = StreamSupport.stream(results.spliterator(), false).collect(Collectors.toList()); + assertThat("Index should contain one result", nodes.size(), equalTo(1)); + assertThat("Should find correct Geometry", encoder.decodeGeometry(nodes.get(0)), + equalTo(geometryFactory.createPoint(new Coordinate(1.0, 1.0)))); + tx.commit(); + } + } - @Test - public void shouldFindOnlyOneOfTwoNodesAddedDirectlyToIndex() { - SpatialIndexWriter index = mockLayerIndex(); - addSimplePoint(index, 10.0, 10.0); - addSimplePoint(index, 1.0, 1.0); - try (Transaction tx = graph.beginTx()) { - SearchResults results = index.searchIndex(tx, new SearchIntersectWindow(((LayerIndexReader) index).getLayer(), new Envelope(0.0, 2.0, 0.0, 2.0))); - List nodes = StreamSupport.stream(results.spliterator(), false).collect(Collectors.toList()); - assertThat("Index should contain one result", nodes.size(), equalTo(1)); - assertThat("Should find correct Geometry", encoder.decodeGeometry(nodes.get(0)), equalTo(geometryFactory.createPoint(new Coordinate(1.0, 1.0)))); - tx.commit(); - } - } + @Test + public void shouldFindOnlyOneOfTwoNodesAddedDirectlyToIndex() { + SpatialIndexWriter index = mockLayerIndex(); + addSimplePoint(index, 10.0, 10.0); + addSimplePoint(index, 1.0, 1.0); + try (Transaction tx = graph.beginTx()) { + SearchResults results = index.searchIndex(tx, + new SearchIntersectWindow(((LayerIndexReader) index).getLayer(), new Envelope(0.0, 2.0, 0.0, 2.0))); + List nodes = StreamSupport.stream(results.spliterator(), false).collect(Collectors.toList()); + assertThat("Index should contain one result", nodes.size(), equalTo(1)); + assertThat("Should find correct Geometry", encoder.decodeGeometry(nodes.get(0)), + equalTo(geometryFactory.createPoint(new Coordinate(1.0, 1.0)))); + tx.commit(); + } + } - private Polygon makeTestPolygonInSquare(GeometryFactory geometryFactory, int length) { - if (length < 4) { - throw new IllegalArgumentException("Cannot create letter C in square smaller than 4x4"); - } - int maxDim = length - 1; - LinearRing shell = geometryFactory.createLinearRing(new Coordinate[]{ - new Coordinate(0, 1), - new Coordinate(0, maxDim - 1), - new Coordinate(1, maxDim), - new Coordinate(maxDim, maxDim), - new Coordinate(maxDim, maxDim - 1), - new Coordinate(1, maxDim - 1), - new Coordinate(1, 1), - new Coordinate(maxDim, 1), - new Coordinate(maxDim, 0), - new Coordinate(1, 0), - new Coordinate(0, 1) - }); - return geometryFactory.createPolygon(shell); - } + private Polygon makeTestPolygonInSquare(GeometryFactory geometryFactory, int length) { + if (length < 4) { + throw new IllegalArgumentException("Cannot create letter C in square smaller than 4x4"); + } + int maxDim = length - 1; + LinearRing shell = geometryFactory.createLinearRing(new Coordinate[]{ + new Coordinate(0, 1), + new Coordinate(0, maxDim - 1), + new Coordinate(1, maxDim), + new Coordinate(maxDim, maxDim), + new Coordinate(maxDim, maxDim - 1), + new Coordinate(1, maxDim - 1), + new Coordinate(1, 1), + new Coordinate(maxDim, 1), + new Coordinate(maxDim, 0), + new Coordinate(1, 0), + new Coordinate(0, 1) + }); + return geometryFactory.createPolygon(shell); + } - @Test - public void shouldFindCorrectSetOfNodesInsideAndOnPolygonEdge() { - int length = 5; // make 5x5 square to test on - SimplePointLayer layer = makeTestPointLayer(); - GeometryFactory geometryFactory = layer.getGeometryFactory(); - Polygon polygon = makeTestPolygonInSquare(geometryFactory, length); - HashSet notIncluded = new LinkedHashSet<>(); - HashSet included = new LinkedHashSet<>(); - for (int x = 0; x < length; x++) { - for (int y = 0; y < length; y++) { - try (Transaction tx = graph.beginTx()) { - Coordinate coordinate = new Coordinate(x, y); - layer.add(tx, coordinate); - Geometry point = geometryFactory.createPoint(coordinate); - if (polygon.intersects(point)) { - included.add(coordinate); - } else { - notIncluded.add(coordinate); - } - tx.commit(); - } - } - } - try (Transaction tx = graph.beginTx()) { - SearchResults results = layer.getIndex().searchIndex(tx, new SearchIntersect(layer, polygon)); - Set found = StreamSupport.stream(results.spliterator(), false).map(n -> - layer.getGeometryEncoder().decodeGeometry(n).getCoordinate() - ).collect(Collectors.toSet()); - assertThat("Index should contain expected number of results", found.size(), equalTo(included.size())); - assertThat("Should find correct Geometries", found, equalTo(included)); - for (Coordinate shouldNotBeFound : notIncluded) { - assertThat("Point should not have been found", found, not(hasItem(shouldNotBeFound))); - } - tx.commit(); - } - } + @Test + public void shouldFindCorrectSetOfNodesInsideAndOnPolygonEdge() { + int length = 5; // make 5x5 square to test on + SimplePointLayer layer = makeTestPointLayer(); + GeometryFactory geometryFactory = layer.getGeometryFactory(); + Polygon polygon = makeTestPolygonInSquare(geometryFactory, length); + HashSet notIncluded = new LinkedHashSet<>(); + HashSet included = new LinkedHashSet<>(); + for (int x = 0; x < length; x++) { + for (int y = 0; y < length; y++) { + try (Transaction tx = graph.beginTx()) { + Coordinate coordinate = new Coordinate(x, y); + layer.add(tx, coordinate); + Geometry point = geometryFactory.createPoint(coordinate); + if (polygon.intersects(point)) { + included.add(coordinate); + } else { + notIncluded.add(coordinate); + } + tx.commit(); + } + } + } + try (Transaction tx = graph.beginTx()) { + SearchResults results = layer.getIndex().searchIndex(tx, new SearchIntersect(layer, polygon)); + Set found = StreamSupport.stream(results.spliterator(), false).map(n -> + layer.getGeometryEncoder().decodeGeometry(n).getCoordinate() + ).collect(Collectors.toSet()); + assertThat("Index should contain expected number of results", found.size(), equalTo(included.size())); + assertThat("Should find correct Geometries", found, equalTo(included)); + for (Coordinate shouldNotBeFound : notIncluded) { + assertThat("Point should not have been found", found, not(hasItem(shouldNotBeFound))); + } + tx.commit(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeNativePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeNativePointIndexTest.java index 6c2da990f..ef548ddfb 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeNativePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeNativePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerRTreeNativePointIndexTest extends NativePointIndexTestBase { - protected Class getIndexClass() { - return LayerRTreeIndex.class; - } + protected Class getIndexClass() { + return LayerRTreeIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerRTreeIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerRTreeIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeSimplePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeSimplePointIndexTest.java index df38d3ff0..ace016a5f 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeSimplePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerRTreeSimplePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerRTreeSimplePointIndexTest extends SimplePointIndexTestBase { - protected Class getIndexClass() { - return LayerRTreeIndex.class; - } + protected Class getIndexClass() { + return LayerRTreeIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerRTreeIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerRTreeIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderNativePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderNativePointIndexTest.java index b1f3595b7..19f3e5aa1 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderNativePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderNativePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerZOrderNativePointIndexTest extends NativePointIndexTestBase { - protected Class getIndexClass() { - return LayerZOrderPointIndex.class; - } + protected Class getIndexClass() { + return LayerZOrderPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerZOrderPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerZOrderPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderSimplePointIndexTest.java b/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderSimplePointIndexTest.java index 7931e1e6a..ec37c907f 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderSimplePointIndexTest.java +++ b/src/test/java/org/neo4j/gis/spatial/index/LayerZOrderSimplePointIndexTest.java @@ -21,12 +21,12 @@ public class LayerZOrderSimplePointIndexTest extends SimplePointIndexTestBase { - protected Class getIndexClass() { - return LayerZOrderPointIndex.class; - } + protected Class getIndexClass() { + return LayerZOrderPointIndex.class; + } - @Override - protected LayerIndexReader makeIndex() { - return new LayerZOrderPointIndex(); - } + @Override + protected LayerIndexReader makeIndex() { + return new LayerZOrderPointIndex(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/NativePointIndexTestBase.java b/src/test/java/org/neo4j/gis/spatial/index/NativePointIndexTestBase.java index 6f09b7a8a..218f68bd2 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/NativePointIndexTestBase.java +++ b/src/test/java/org/neo4j/gis/spatial/index/NativePointIndexTestBase.java @@ -24,13 +24,13 @@ public abstract class NativePointIndexTestBase extends LayerIndexTestBase { - @Override - protected Class getEncoderClass() { - return NativePointEncoder.class; - } + @Override + protected Class getEncoderClass() { + return NativePointEncoder.class; + } - @Override - protected final GeometryEncoder makeGeometryEncoder() { - return new NativePointEncoder(); - } + @Override + protected final GeometryEncoder makeGeometryEncoder() { + return new NativePointEncoder(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/index/SimplePointIndexTestBase.java b/src/test/java/org/neo4j/gis/spatial/index/SimplePointIndexTestBase.java index 95dc9ad54..0de7e2269 100644 --- a/src/test/java/org/neo4j/gis/spatial/index/SimplePointIndexTestBase.java +++ b/src/test/java/org/neo4j/gis/spatial/index/SimplePointIndexTestBase.java @@ -24,13 +24,13 @@ public abstract class SimplePointIndexTestBase extends LayerIndexTestBase { - @Override - protected Class getEncoderClass() { - return SimplePointEncoder.class; - } + @Override + protected Class getEncoderClass() { + return SimplePointEncoder.class; + } - @Override - protected final GeometryEncoder makeGeometryEncoder() { - return new SimplePointEncoder(); - } + @Override + protected final GeometryEncoder makeGeometryEncoder() { + return new SimplePointEncoder(); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesDocTest.java b/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesDocTest.java index 2d3888a34..070be1a14 100644 --- a/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesDocTest.java +++ b/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesDocTest.java @@ -19,13 +19,24 @@ */ package org.neo4j.gis.spatial.pipes; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.NoSuchElementException; +import org.geotools.api.style.Style; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.geotools.data.neo4j.StyledImageExporter; import org.geotools.feature.FeatureCollection; import org.geotools.filter.text.cql2.CQLException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.crs.DefaultEngineeringCRS; -import org.geotools.api.style.Style; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -37,7 +48,11 @@ import org.locationtech.jts.io.ParseException; import org.locationtech.jts.io.WKTReader; import org.neo4j.annotations.documented.Documented; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.AbstractJavaDocTestBase; +import org.neo4j.gis.spatial.Constants; +import org.neo4j.gis.spatial.EditableLayerImpl; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.SpatialDatabaseService; import org.neo4j.gis.spatial.filter.SearchIntersectWindow; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.osm.OSMImporter; @@ -51,992 +66,1010 @@ import org.neo4j.test.TestData.Title; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.awt.*; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.NoSuchElementException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; - public class GeoPipesDocTest extends AbstractJavaDocTestBase { - private static Layer osmLayer; - private static EditableLayerImpl boxesLayer; - private static EditableLayerImpl concaveLayer; - private static EditableLayerImpl intersectionLayer; - private static EditableLayerImpl equalLayer; - private static EditableLayerImpl linesLayer; - private Transaction tx; - - @Test - public void find_all() { - int count = 0; - for (GeoPipeFlow flow : GeoPipeline.start(tx, osmLayer).createWellKnownText()) { - count++; - - assertEquals(1, flow.getProperties().size()); - String wkt = (String) flow.getProperties().get("WellKnownText"); - assertEquals(0, wkt.indexOf("LINESTRING")); - } - - assertEquals(2, count); - } - - @Test - public void filter_by_osm_attribute() { - GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer) - .osmAttributeFilter("name", "Storgatan") - .copyDatabaseRecordProperties(tx); - - GeoPipeFlow flow = pipeline.next(); - assertFalse(pipeline.hasNext()); - - assertEquals("Storgatan", flow.getProperties().get("name")); - } - - @Test - public void filter_by_property() { - GeoPipeline pipeline = GeoPipeline.start(tx, osmLayer) - .copyDatabaseRecordProperties(tx, "name") - .propertyFilter("name", "Storgatan"); - - GeoPipeFlow flow = pipeline.next(); - assertFalse(pipeline.hasNext()); - - assertEquals("Storgatan", flow.getProperties().get("name")); - } - - @Test - public void filter_by_window_intersection() { - assertEquals(1, GeoPipeline.start(tx, osmLayer).windowIntersectionFilter(10, 40, 20, 56.0583531).count()); - } - - /** - * This pipe is filtering according to a CQL Bounding Box description. - *

- * Example: - * - * @@s_filter_by_cql_using_bbox - */ - @Documented("filter_by_cql_using_bbox") - @Test - public void filter_by_cql_using_bbox() throws CQLException { - // tag::filter_by_cql_using_bbox[] - GeoPipeline cqlFilter = GeoPipeline.start(tx, osmLayer).cqlFilter(tx, "BBOX(the_geom, 10, 40, 20, 56.0583531)"); - // end::filter_by_cql_using_bbox[] - assertEquals(1, cqlFilter.count()); - } - - /** - * This pipe performs a search within a geometry in this example, - * both OSM street geometries should be found in when searching with - * an enclosing rectangle Envelope. - *

- * Example: - * - * @@s_search_within_geometry - */ - @Test - @Documented("search_within_geometry") - public void search_within_geometry() throws CQLException { - // tag::search_within_geometry[] - GeoPipeline pipeline = GeoPipeline - .startWithinSearch(tx, osmLayer, osmLayer.getGeometryFactory().toGeometry(new Envelope(10, 20, 50, 60))); - // end::search_within_geometry[] - assertEquals(2, pipeline.count()); - } - - @Test - public void filter_by_cql_using_property() throws CQLException { - GeoPipeline pipeline = GeoPipeline.start(tx, osmLayer).cqlFilter(tx, "name = 'Storgatan'").copyDatabaseRecordProperties(tx); - - GeoPipeFlow flow = pipeline.next(); - assertFalse(pipeline.hasNext()); - - assertEquals("Storgatan", flow.getProperties().get("name")); - } - - /** - * This filter will apply the provided CQL expression to the different - * geometries and only let the matching ones pass. - *

- * Example: - * - * @@s_filter_by_cql_using_complex_cql - */ - @Documented("filter_by_cql_using_complex_cql") - @Test - public void filter_by_cql_using_complex_cql() throws CQLException { - // tag::filter_by_cql_using_complex_cql[] - long counter = GeoPipeline.start(tx, osmLayer).cqlFilter(tx, "highway is not null and geometryType(the_geom) = 'LineString'").count(); - // end::filter_by_cql_using_complex_cql[] - - FilterCQL filter = new FilterCQL(tx, osmLayer, "highway is not null and geometryType(the_geom) = 'LineString'"); - filter.setStarts(GeoPipeline.start(tx, osmLayer)); - assertTrue(filter.hasNext()); - while (filter.hasNext()) { - filter.next(); - counter--; - } - assertEquals(0, counter); - } - - /** - * Affine Transformation - *

- * The ApplyAffineTransformation pipe applies an affine transformation to every geometry. - *

- * Example: - * - * @@s_affine_transformation Output: - * @@affine_transformation - */ - @Documented("translate_geometries") - @Test - public void translate_geometries() { - // tag::affine_transformation[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) - .applyAffineTransform(AffineTransformation.translationInstance(2, 3)); - // end::affine_transformation[] - addImageSnippet(boxesLayer, pipeline, getTitle()); - - GeoPipeline original = GeoPipeline.start(tx, osmLayer).copyDatabaseRecordProperties(tx).sort( - "name"); - - GeoPipeline translated = GeoPipeline.start(tx, osmLayer).applyAffineTransform( - AffineTransformation.translationInstance(10, 25)).copyDatabaseRecordProperties(tx).sort( - "name"); - - for (int k = 0; k < 2; k++) { - Coordinate[] coords = original.next().getGeometry().getCoordinates(); - Coordinate[] newCoords = translated.next().getGeometry().getCoordinates(); - assertEquals(coords.length, newCoords.length); - for (int i = 0; i < coords.length; i++) { - assertEquals(coords[i].x + 10, newCoords[i].x, 0); - assertEquals(coords[i].y + 25, newCoords[i].y, 0); - } - } - } - - @Test - public void calculate_area() { - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().sort("Area"); - - assertEquals((Double) pipeline.next().getProperties().get("Area"), 1.0, 0); - assertEquals((Double) pipeline.next().getProperties().get("Area"), 8.0, 0); - pipeline.reset(); - } - - @Test - public void calculate_length() { - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateLength().sort("Length"); - - assertEquals((Double) pipeline.next().getProperties().get("Length"), 4.0, 0); - assertEquals((Double) pipeline.next().getProperties().get("Length"), 12.0, 0); - pipeline.reset(); - } - - @Test - public void get_boundary_length() { - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBoundary().createWellKnownText().calculateLength().sort("Length"); - - GeoPipeFlow first = pipeline.next(); - GeoPipeFlow second = pipeline.next(); - assertEquals("LINEARRING (12 26, 12 27, 13 27, 13 26, 12 26)", first.getProperties().get("WellKnownText")); - assertEquals("LINEARRING (2 3, 2 5, 6 5, 6 3, 2 3)", second.getProperties().get("WellKnownText")); - assertEquals((Double) first.getProperties().get("Length"), 4.0, 0); - assertEquals((Double) second.getProperties().get("Length"), 12.0, 0); - pipeline.reset(); - } - - /** - * Buffer - *

- * The Buffer pipe applies a buffer to geometries. - *

- * Example: - * - * @@s_buffer Output: - * @@buffer - */ - @Documented("get_buffer") - @Test - public void get_buffer() { - // tag::buffer[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBuffer(0.5); - // end::buffer[] - addImageSnippet(boxesLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, boxesLayer).toBuffer(0.1).createWellKnownText().calculateArea().sort("Area"); - - assertTrue(((Double) pipeline.next().getProperties().get("Area")) > 1); - assertTrue(((Double) pipeline.next().getProperties().get("Area")) > 8); - pipeline.reset(); - } - - /** - * Centroid - *

- * The Centroid pipe calculates geometry centroid. - *

- * Example: - * - * @@s_centroid Output: - * @@centroid - */ - @Documented("get_centroid") - @Test - public void get_centroid() { - // tag::centroid[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toCentroid(); - // end::centroid[] - addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); - - pipeline = GeoPipeline.start(tx, boxesLayer).toCentroid().createWellKnownText().copyDatabaseRecordProperties(tx).sort("name"); - - assertEquals("POINT (12.5 26.5)", pipeline.next().getProperties().get("WellKnownText")); - assertEquals("POINT (4 4)", pipeline.next().getProperties().get("WellKnownText")); - pipeline.reset(); - } - - /** - * This pipe exports every geometry as a - * http://en.wikipedia.org/wiki/Geography_Markup_Language[GML] snippet. - *

- * Example: - * - * @@s_export_to_gml Output: - * @@exportgml - */ - @Documented("export_to_GML") - @Test - public void export_to_GML() { - // tag::export_to_gml[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).createGML(); - for (GeoPipeFlow flow : pipeline) { - System.out.println(flow.getProperties().get("GML")); - } - // end::export_to_gml[] - String result = ""; - for (GeoPipeFlow flow : GeoPipeline.start(tx, boxesLayer).createGML()) { - result = result + flow.getProperties().get("GML"); - } - gen.get().addSnippet("exportgml", "[source,xml]\n----\n" + result + "\n----\n"); - } - - /** - * Convex Hull - *

- * The ConvexHull pipe calculates geometry convex hull. - *

- * Example: - * - * @@s_convex_hull Output: - * @@convex_hull - */ - @Documented("get_convex_hull") - @Test - public void get_convex_hull() { - // tag::convex_hull[] - GeoPipeline pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull(); - // end::convex_hull[] - addImageSnippet(concaveLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull().createWellKnownText(); - - assertEquals("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))", pipeline.next().getProperties().get("WellKnownText")); - pipeline.reset(); - } - - /** - * Densify - *

- * The Densify pipe inserts extra vertices along the line segments in the geometry. - * The densified geometry contains no line segment which is longer than the given distance tolerance. - *

- * Example: - * - * @@s_densify Output: - * @@densify - */ - @Documented("densify") - @Test - public void densify() { - // tag::densify[] - GeoPipeline pipeline = GeoPipeline.start(tx, concaveLayer).densify(5).extractPoints(); - // end::densify[] - addImageSnippet(concaveLayer, pipeline, getTitle(), Constants.GTYPE_POINT); - - pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull().densify(5).createWellKnownText(); - - String wkt = (String) pipeline.next().getProperties().get("WellKnownText"); - pipeline.reset(); - assertEquals("POLYGON ((0 0, 0 5, 0 10, 5 10, 10 10, 10 5, 10 0, 5 0, 0 0))", wkt); - } - - @Test - public void json() { - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).createJson().copyDatabaseRecordProperties(tx).sort("name"); - - assertEquals("{\"type\":\"Polygon\",\"coordinates\":[[[12,26],[12,27],[13,27],[13,26],[12,26]]]}", pipeline.next().getProperties().get("GeoJSON")); - assertEquals("{\"type\":\"Polygon\",\"coordinates\":[[[2,3],[2,5],[6,5],[6,3],[2,3]]]}", pipeline.next().getProperties().get("GeoJSON")); - pipeline.reset(); - } - - /** - * Max - *

- * The Max pipe computes the maximum value of the specified property and - * discard items with a value less than the maximum. - *

- * Example: - * - * @@s_max Output: - * @@max - */ - @Documented("get_max_area") - @Test - public void get_max_area() { - // tag::max[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) - .calculateArea() - .getMax("Area"); - // end::max[] - addImageSnippet(boxesLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().getMax("Area"); - assertEquals((Double) pipeline.next().getProperties().get("Area"), 8.0, 0); - pipeline.reset(); - } - - /** - * The boundary pipe calculates boundary of every geometry in the pipeline. - *

- * Example: - * - * @@s_boundary Output: - * @@boundary - */ - @Documented("boundary") - @Test - public void boundary() { - // tag::boundary[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBoundary(); - // end::boundary[] - addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_LINESTRING); - - // TODO test? - } - - /** - * Difference - *

- * The Difference pipe computes a geometry representing the points making - * up item geometry that do not make up the given geometry. - *

- * Example: - * - * @@s_difference Output: - * @@difference - */ - @Documented("difference.") - @Test - public void difference() throws Exception { - // tag::difference[] - WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); - Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); - GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).difference(geometry); - // end::difference[] - addImageSnippet(intersectionLayer, pipeline, getTitle()); - - // TODO test? - } - - /** - * Intersection - *

- * The Intersection pipe computes a geometry representing the intersection - * between item geometry and the given geometry. - *

- * Example: - * - * @@s_intersection Output: - * @@intersection - */ - @Documented("intersection") - @Test - public void intersection() throws Exception { - // tag::intersection[] - WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); - Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); - GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).intersect(geometry); - // end::intersection[] - addImageSnippet(intersectionLayer, pipeline, getTitle()); - - // TODO test? - } - - /** - * Union - *

- * The Union pipe unites item geometry with a given geometry. - *

- * Example: - * - * @@s_union Output: - * @@union - */ - @Documented("union") - @Test - public void union() throws Exception { - // tag::union[] - WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); - Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); - SearchFilter filter = new SearchIntersectWindow(intersectionLayer, new Envelope(7, 10, 7, 10)); - GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer, filter).union(geometry); - // end::union[] - addImageSnippet(intersectionLayer, pipeline, getTitle()); - - // TODO test? - } - - /** - * Min - *

- * The Min pipe computes the minimum value of the specified property and - * discard items with a value greater than the minimum. - *

- * Example: - * - * @@s_min Output: - * @@min - */ - @Documented("get_min_area") - @Test - public void get_min_area() { - // tag::min[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) - .calculateArea() - .getMin("Area"); - // end::min[] - addImageSnippet(boxesLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().getMin("Area"); - assertEquals((Double) pipeline.next().getProperties().get("Area"), 1.0, 0); - pipeline.reset(); - } - - @Test - public void extract_osm_points() { - int count = 0; - GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().createWellKnownText(); - for (GeoPipeFlow flow : pipeline) { - count++; - - assertEquals(1, flow.getProperties().size()); - String wkt = (String) flow.getProperties().get("WellKnownText"); - assertEquals(0, wkt.indexOf("POINT")); - } - - assertEquals(24, count); - } - - /** - * A more complex Open Street Map example. - *

- * This example demostrates the some pipes chained together to make a full - * geoprocessing pipeline. - *

- * Example: - * - * @@s_break_up_all_geometries_into_points_and_make_density_islands _Step1_ - * @@step1_break_up_all_geometries_into_points_and_make_density_islands _Step2_ - * @@step2_break_up_all_geometries_into_points_and_make_density_islands _Step3_ - * @@step3_break_up_all_geometries_into_points_and_make_density_islands _Step4_ - * @@step4_break_up_all_geometries_into_points_and_make_density_islands _Step5_ - * @@step5_break_up_all_geometries_into_points_and_make_density_islands - */ - @Documented("break_up_all_geometries_into_points_and_make_density_islands_and_get_the_outer_linear_ring_of_the_density_islands_and_buffer_the_geometry_and_count_them") - @Title("break_up_all_geometries_into_points_and_make_density_islands") - @Test - public void break_up_all_geometries_into_points_and_make_density_islands_and_get_the_outer_linear_ring_of_the_density_islands_and_buffer_the_geometry_and_count_them() { - // tag::break_up_all_geometries_into_points_and_make_density_islands[] - //step1 - GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer) - //step2 - .extractOsmPoints() - //step3 - .groupByDensityIslands(0.0005) - //step4 - .toConvexHull() - //step5 - .toBuffer(0.0004); - // end::break_up_all_geometries_into_points_and_make_density_islands[] - - assertEquals(9, pipeline.count()); - - addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer), "step1_" + getTitle(), Constants.GTYPE_LINESTRING); - addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints(), "step2_" + getTitle(), Constants.GTYPE_POINT); - addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005), "step3_" + getTitle(), Constants.GTYPE_POLYGON); - addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005).toConvexHull(), "step4_" + getTitle(), Constants.GTYPE_POLYGON); - addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005).toConvexHull().toBuffer(0.0004), "step5_" + getTitle(), Constants.GTYPE_POLYGON); - } - - /** - * Extract Points - *

- * The Extract Points pipe extracts every point from a geometry. - *

- * Example: - * - * @@s_extract_points Output: - * @@extract_points - */ - @Documented("extract_points") - @Test - public void extract_points() { - // tag::extract_points[] - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).extractPoints(); - // end::extract_points[] - addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); - - int count = 0; - for (GeoPipeFlow flow : GeoPipeline.start(tx, boxesLayer).extractPoints().createWellKnownText()) { - count++; - - assertEquals(1, flow.getProperties().size()); - String wkt = (String) flow.getProperties().get("WellKnownText"); - assertTrue(wkt.indexOf("POINT") == 0); - } - - // every rectangle has 5 points, the last point is in the same position of the first - assertEquals(10, count); - } - - @Test - public void filter_by_null_property() { - assertEquals(2, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNullFilter("address").count()); - assertEquals(0, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNullFilter("name").count()); - } - - @Test - public void filter_by_not_null_property() { - assertEquals(0, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNotNullFilter("address").count()); - assertEquals(2, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNotNullFilter("name").count()); - } - - @Test - public void compute_distance() throws ParseException { - WKTReader reader = new WKTReader(boxesLayer.getGeometryFactory()); - - GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateDistance( - reader.read("POINT (0 0)")).sort("Distance"); - - assertEquals(4, Math.round((Double) pipeline.next().getProperty(tx, "Distance"))); - assertEquals(29, Math.round((Double) pipeline.next().getProperty(tx, "Distance"))); - pipeline.reset(); - } - - /** - * Unite All - *

- * The Union All pipe unites geometries of every item contained in the pipeline. - * This pipe groups every item in the pipeline in a single item containing the geometry output - * of the union. - *

- * Example: - * - * @@s_unite_all Output: - * @@unite_all - */ - @Documented("unite_all") - @Test - public void unite_all() { - // tag::unite_all[] - GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).unionAll(); - // end::unite_all[] - addImageSnippet(intersectionLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, intersectionLayer) - .unionAll() - .createWellKnownText(); - - assertEquals("POLYGON ((2 5, 2 6, 4 6, 4 10, 10 10, 10 4, 6 4, 6 2, 5 2, 5 0, 0 0, 0 5, 2 5))", pipeline.next().getProperty(tx, "WellKnownText")); - - try { - pipeline.next(); - fail(); - } catch (NoSuchElementException ignored) { - } - } - - /** - * Intersect All - *

- * The IntersectAll pipe intersects geometries of every item contained in the pipeline. - * It groups every item in the pipeline in a single item containing the geometry output - * of the intersection. - *

- * Example: - * - * @@s_intersect_all Output: - * @@intersect_all - */ - @Documented("intersect_all") - @Test - public void intersect_all() { - // tag::intersect_all[] - GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).intersectAll(); - // end::intersect_all[] - addImageSnippet(intersectionLayer, pipeline, getTitle()); - - pipeline = GeoPipeline.start(tx, intersectionLayer) - .intersectAll() - .createWellKnownText(); - - assertEquals("POLYGON ((4 5, 5 5, 5 4, 4 4, 4 5))", pipeline.next().getProperty(tx, "WellKnownText")); - - try { - pipeline.next(); - fail(); - } catch (NoSuchElementException ignored) { - } - } - - /** - * Intersecting windows - *

- * The FilterIntersectWindow pipe finds geometries that intersects a given rectangle. - * This pipeline: - * - * @@s_intersecting_windows will output: - * @@intersecting_windows - */ - @Documented("intersecting_windows") - @Test - public void intersecting_windows() { - // tag::intersecting_windows[] - GeoPipeline pipeline = GeoPipeline - .start(tx, boxesLayer) - .windowIntersectionFilter(new Envelope(0, 10, 0, 10)); - // end::intersecting_windows[] - addImageSnippet(boxesLayer, pipeline, getTitle()); - - // TODO test? - } - - /** - * Start Point - *

- * The StartPoint pipe finds the starting point of item geometry. - *

- * Example: - * - * @@s_start_point Output: - * @@start_point - */ - @Documented("start_point") - @Test - public void start_point() { - // tag::start_point[] - GeoPipeline pipeline = GeoPipeline - .start(tx, linesLayer) - .toStartPoint(); - // end::start_point[] - addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); - - pipeline = GeoPipeline - .start(tx, linesLayer) - .toStartPoint() - .createWellKnownText(); - - assertEquals("POINT (12 26)", pipeline.next().getProperty(tx, "WellKnownText")); - pipeline.reset(); - } - - /** - * End Point - *

- * The EndPoint pipe finds the ending point of item geometry. - *

- * Example: - * - * @@s_end_point Output: - * @@end_point - */ - @Documented("end_point") - @Test - public void end_point() { - // tag::end_point[] - GeoPipeline pipeline = GeoPipeline - .start(tx, linesLayer) - .toEndPoint(); - // end::end_point[] - addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); - - pipeline = GeoPipeline - .start(tx, linesLayer) - .toEndPoint() - .createWellKnownText(); - - assertEquals("POINT (23 34)", pipeline.next().getProperty(tx, "WellKnownText")); - pipeline.reset(); - } - - /** - * Envelope - *

- * The Envelope pipe computes the minimum bounding box of item geometry. - *

- * Example: - * - * @@s_envelope Output: - * @@envelope - */ - @Documented("envelope") - @Test - public void envelope() { - // tag::envelope[] - GeoPipeline pipeline = GeoPipeline - .start(tx, linesLayer) - .toEnvelope(); - // end::envelope[] - addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POLYGON); - - // TODO test - } - - @Test - public void test_equality() throws Exception { - WKTReader reader = new WKTReader(equalLayer.getGeometryFactory()); - Geometry geom = reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))"); - - GeoPipeline pipeline = GeoPipeline.startEqualExactSearch(tx, equalLayer, geom, 0).copyDatabaseRecordProperties(tx); - assertEquals("equal", pipeline.next().getProperty(tx, "name")); - assertFalse(pipeline.hasNext()); - - pipeline = GeoPipeline.startEqualExactSearch(tx, equalLayer, geom, 0.1).copyDatabaseRecordProperties(tx).sort("id"); - assertEquals("equal", pipeline.next().getProperty(tx, "name")); - assertEquals("tolerance", pipeline.next().getProperty(tx, "name")); - assertFalse(pipeline.hasNext()); - - pipeline = GeoPipeline.startIntersectWindowSearch(tx, equalLayer, - geom.getEnvelopeInternal()).equalNormFilter(geom, 0.1).copyDatabaseRecordProperties(tx).sort("id"); - assertEquals("equal", pipeline.next().getProperty(tx, "name")); - assertEquals("tolerance", pipeline.next().getProperty(tx, "name")); - assertEquals("different order", pipeline.next().getProperty(tx, "name")); - assertFalse(pipeline.hasNext()); - - pipeline = GeoPipeline.startIntersectWindowSearch(tx, equalLayer, - geom.getEnvelopeInternal()).equalTopoFilter(geom).copyDatabaseRecordProperties(tx).sort("id"); - assertEquals("equal", pipeline.next().getProperty(tx, "name")); - assertEquals("different order", pipeline.next().getProperty(tx, "name")); - assertEquals("topo equal", pipeline.next().getProperty(tx, "name")); - assertFalse(pipeline.hasNext()); - pipeline.reset(); - } - - private String getTitle() { - return gen.get().getTitle().replace(" ", "_").toLowerCase(); - } - - private void addImageSnippet( - Layer layer, - GeoPipeline pipeline, - String imgName) { - addImageSnippet(layer, pipeline, imgName, null); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private void addOsmImageSnippet( - Layer layer, - GeoPipeline pipeline, - String imgName, - Integer geomType) { - addImageSnippet(layer, pipeline, imgName, geomType, 0.002); - } - - private void addImageSnippet( - Layer layer, - GeoPipeline pipeline, - String imgName, - Integer geomType) { - addImageSnippet(layer, pipeline, imgName, geomType, 1); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private void addImageSnippet( - Layer layer, - GeoPipeline pipeline, - String imgName, - Integer geomType, - double boundsDelta) { - gen.get().addSnippet(imgName, "\nimage::" + imgName + ".png[scaledwidth=\"75%\"]\n"); - - try { - FeatureCollection layerCollection = GeoPipeline.start(tx, layer, new SearchAll()).toFeatureCollection(tx); - FeatureCollection pipelineCollection; - if (geomType == null) { - pipelineCollection = pipeline.toFeatureCollection(tx); - } else { - pipelineCollection = pipeline.toFeatureCollection(tx, - Neo4jFeatureBuilder.getType(layer.getName(), geomType, layer.getCoordinateReferenceSystem(tx), layer.getExtraPropertyNames(tx))); - } - - ReferencedEnvelope bounds = layerCollection.getBounds(); - bounds.expandToInclude(pipelineCollection.getBounds()); - bounds.expandBy(boundsDelta, boundsDelta); - - StyledImageExporter exporter = new StyledImageExporter(db); - exporter.setExportDir("target/docs/images/"); - exporter.saveImage( - new FeatureCollection[]{ - layerCollection, - pipelineCollection, - }, - new Style[]{ - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), - StyledImageExporter.createDefaultStyle(Color.RED, Color.ORANGE) - }, - new File(imgName + ".png"), - bounds); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void load() throws Exception { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); - - try (Transaction tx = db.beginTx()) { - loadTestOsmData("two-street.osm", 100); - osmLayer = spatial.getLayer(tx, "two-street.osm"); - - boxesLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "boxes"); - boxesLayer.setExtraPropertyNames(new String[]{"name"}, tx); - boxesLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); - WKTReader reader = new WKTReader(boxesLayer.getGeometryFactory()); - boxesLayer.add(tx, - reader.read("POLYGON ((12 26, 12 27, 13 27, 13 26, 12 26))"), - new String[]{"name"}, new Object[]{"A"}); - boxesLayer.add(tx, - reader.read("POLYGON ((2 3, 2 5, 6 5, 6 3, 2 3))"), - new String[]{"name"}, new Object[]{"B"}); - - concaveLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "concave"); - concaveLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); - reader = new WKTReader(concaveLayer.getGeometryFactory()); - concaveLayer.add(tx, reader.read("POLYGON ((0 0, 2 5, 0 10, 10 10, 10 0, 0 0))")); - - intersectionLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "intersection"); - intersectionLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); - reader = new WKTReader(intersectionLayer.getGeometryFactory()); - intersectionLayer.add(tx, reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))")); - intersectionLayer.add(tx, reader.read("POLYGON ((4 4, 4 10, 10 10, 10 4, 4 4))")); - intersectionLayer.add(tx, reader.read("POLYGON ((2 2, 2 6, 6 6, 6 2, 2 2))")); - - equalLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "equal"); - equalLayer.setExtraPropertyNames(new String[]{"id", "name"}, tx); - equalLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); - reader = new WKTReader(intersectionLayer.getGeometryFactory()); - equalLayer.add(tx, reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))"), - new String[]{"id", "name"}, new Object[]{1, "equal"}); - equalLayer.add(tx, reader.read("POLYGON ((0 0, 0.1 5, 5 5, 5 0, 0 0))"), - new String[]{"id", "name"}, new Object[]{2, "tolerance"}); - equalLayer.add(tx, reader.read("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 5))"), - new String[]{"id", "name"}, new Object[]{3, - "different order"}); - equalLayer.add(tx, - reader.read("POLYGON ((0 0, 0 2, 0 4, 0 5, 5 5, 5 3, 5 2, 5 0, 0 0))"), - new String[]{"id", "name"}, new Object[]{4, "topo equal"}); - - linesLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "lines"); - linesLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); - reader = new WKTReader(intersectionLayer.getGeometryFactory()); - linesLayer.add(tx, reader.read("LINESTRING (12 26, 15 27, 18 32, 20 38, 23 34)")); - - tx.commit(); - } - } - - @SuppressWarnings("SameParameterValue") - private static void loadTestOsmData(String layerName, int commitInterval) - throws Exception { - String osmPath = "./" + layerName; - System.out.println("\n=== Loading layer " + layerName + " from " - + osmPath + " ==="); - OSMImporter importer = new OSMImporter(layerName); - importer.setCharset(StandardCharsets.UTF_8); - importer.importFile(db, osmPath); - importer.reIndex(db, commitInterval); - } - - @BeforeEach - public void setUp() { - gen.get().setGraph(db); - try (Transaction tx = db.beginTx()) { - StyledImageExporter exporter = new StyledImageExporter(db); - exporter.setExportDir("target/docs/images/"); - exporter.saveImage(GeoPipeline.start(tx, intersectionLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "intersectionLayer.png")); - - exporter.saveImage(GeoPipeline.start(tx, boxesLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "boxesLayer.png")); - - exporter.saveImage(GeoPipeline.start(tx, concaveLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "concaveLayer.png")); - - exporter.saveImage(GeoPipeline.start(tx, equalLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "equalLayer.png")); - exporter.saveImage(GeoPipeline.start(tx, linesLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "linesLayer.png")); - exporter.saveImage(GeoPipeline.start(tx, osmLayer).toFeatureCollection(tx), - StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( - "osmLayer.png")); - tx.commit(); - } catch (IOException e) { - e.printStackTrace(); - } - tx = db.beginTx(); - } - - @AfterEach - public void doc() { - // gen.get().addSnippet( "graph", AsciidocHelper.createGraphViz( imgName , graphdb(), "graph"+getTitle() ) ); - gen.get().addTestSourceSnippets(GeoPipesDocTest.class, "s_" + getTitle().toLowerCase()); - gen.get().document("target/docs", "examples"); - if (tx != null) { - tx.commit(); - tx.close(); - } - } - - @BeforeAll - public static void init() { - databases = new TestDatabaseManagementServiceBuilder(new File("target/docs").toPath()).impermanent().build(); - db = databases.database(DEFAULT_DATABASE_NAME); - try { - load(); - } catch (Exception e) { - e.printStackTrace(); - } - - StyledImageExporter exporter = new StyledImageExporter(db); - exporter.setExportDir("target/docs/images/"); - } - - private GeoPipeFlow print(GeoPipeFlow pipeFlow) { - System.out.println("GeoPipeFlow:"); - for (String key : pipeFlow.getProperties().keySet()) { - System.out.println(key + "=" + pipeFlow.getProperties().get(key)); - } - System.out.println("-"); - return pipeFlow; - } + + private static Layer osmLayer; + private static EditableLayerImpl boxesLayer; + private static EditableLayerImpl concaveLayer; + private static EditableLayerImpl intersectionLayer; + private static EditableLayerImpl equalLayer; + private static EditableLayerImpl linesLayer; + private Transaction tx; + + @Test + public void find_all() { + int count = 0; + for (GeoPipeFlow flow : GeoPipeline.start(tx, osmLayer).createWellKnownText()) { + count++; + + assertEquals(1, flow.getProperties().size()); + String wkt = (String) flow.getProperties().get("WellKnownText"); + assertEquals(0, wkt.indexOf("LINESTRING")); + } + + assertEquals(2, count); + } + + @Test + public void filter_by_osm_attribute() { + GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer) + .osmAttributeFilter("name", "Storgatan") + .copyDatabaseRecordProperties(tx); + + GeoPipeFlow flow = pipeline.next(); + assertFalse(pipeline.hasNext()); + + assertEquals("Storgatan", flow.getProperties().get("name")); + } + + @Test + public void filter_by_property() { + GeoPipeline pipeline = GeoPipeline.start(tx, osmLayer) + .copyDatabaseRecordProperties(tx, "name") + .propertyFilter("name", "Storgatan"); + + GeoPipeFlow flow = pipeline.next(); + assertFalse(pipeline.hasNext()); + + assertEquals("Storgatan", flow.getProperties().get("name")); + } + + @Test + public void filter_by_window_intersection() { + assertEquals(1, GeoPipeline.start(tx, osmLayer).windowIntersectionFilter(10, 40, 20, 56.0583531).count()); + } + + /** + * This pipe is filtering according to a CQL Bounding Box description. + *

+ * Example: + * + * @@s_filter_by_cql_using_bbox + */ + @Documented("filter_by_cql_using_bbox") + @Test + public void filter_by_cql_using_bbox() throws CQLException { + // tag::filter_by_cql_using_bbox[] + GeoPipeline cqlFilter = GeoPipeline.start(tx, osmLayer).cqlFilter(tx, "BBOX(the_geom, 10, 40, 20, 56.0583531)"); + // end::filter_by_cql_using_bbox[] + assertEquals(1, cqlFilter.count()); + } + + /** + * This pipe performs a search within a geometry in this example, + * both OSM street geometries should be found in when searching with + * an enclosing rectangle Envelope. + *

+ * Example: + * + * @@s_search_within_geometry + */ + @Test + @Documented("search_within_geometry") + public void search_within_geometry() throws CQLException { + // tag::search_within_geometry[] + GeoPipeline pipeline = GeoPipeline + .startWithinSearch(tx, osmLayer, + osmLayer.getGeometryFactory().toGeometry(new Envelope(10, 20, 50, 60))); + // end::search_within_geometry[] + assertEquals(2, pipeline.count()); + } + + @Test + public void filter_by_cql_using_property() throws CQLException { + GeoPipeline pipeline = GeoPipeline.start(tx, osmLayer).cqlFilter(tx, "name = 'Storgatan'") + .copyDatabaseRecordProperties(tx); + + GeoPipeFlow flow = pipeline.next(); + assertFalse(pipeline.hasNext()); + + assertEquals("Storgatan", flow.getProperties().get("name")); + } + + /** + * This filter will apply the provided CQL expression to the different + * geometries and only let the matching ones pass. + *

+ * Example: + * + * @@s_filter_by_cql_using_complex_cql + */ + @Documented("filter_by_cql_using_complex_cql") + @Test + public void filter_by_cql_using_complex_cql() throws CQLException { + // tag::filter_by_cql_using_complex_cql[] + long counter = GeoPipeline.start(tx, osmLayer) + .cqlFilter(tx, "highway is not null and geometryType(the_geom) = 'LineString'").count(); + // end::filter_by_cql_using_complex_cql[] + + FilterCQL filter = new FilterCQL(tx, osmLayer, "highway is not null and geometryType(the_geom) = 'LineString'"); + filter.setStarts(GeoPipeline.start(tx, osmLayer)); + assertTrue(filter.hasNext()); + while (filter.hasNext()) { + filter.next(); + counter--; + } + assertEquals(0, counter); + } + + /** + * Affine Transformation + *

+ * The ApplyAffineTransformation pipe applies an affine transformation to every geometry. + *

+ * Example: + * + * @@s_affine_transformation Output: + * @@affine_transformation + */ + @Documented("translate_geometries") + @Test + public void translate_geometries() { + // tag::affine_transformation[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) + .applyAffineTransform(AffineTransformation.translationInstance(2, 3)); + // end::affine_transformation[] + addImageSnippet(boxesLayer, pipeline, getTitle()); + + GeoPipeline original = GeoPipeline.start(tx, osmLayer).copyDatabaseRecordProperties(tx).sort( + "name"); + + GeoPipeline translated = GeoPipeline.start(tx, osmLayer).applyAffineTransform( + AffineTransformation.translationInstance(10, 25)).copyDatabaseRecordProperties(tx).sort( + "name"); + + for (int k = 0; k < 2; k++) { + Coordinate[] coords = original.next().getGeometry().getCoordinates(); + Coordinate[] newCoords = translated.next().getGeometry().getCoordinates(); + assertEquals(coords.length, newCoords.length); + for (int i = 0; i < coords.length; i++) { + assertEquals(coords[i].x + 10, newCoords[i].x, 0); + assertEquals(coords[i].y + 25, newCoords[i].y, 0); + } + } + } + + @Test + public void calculate_area() { + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().sort("Area"); + + assertEquals((Double) pipeline.next().getProperties().get("Area"), 1.0, 0); + assertEquals((Double) pipeline.next().getProperties().get("Area"), 8.0, 0); + pipeline.reset(); + } + + @Test + public void calculate_length() { + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateLength().sort("Length"); + + assertEquals((Double) pipeline.next().getProperties().get("Length"), 4.0, 0); + assertEquals((Double) pipeline.next().getProperties().get("Length"), 12.0, 0); + pipeline.reset(); + } + + @Test + public void get_boundary_length() { + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBoundary().createWellKnownText().calculateLength() + .sort("Length"); + + GeoPipeFlow first = pipeline.next(); + GeoPipeFlow second = pipeline.next(); + assertEquals("LINEARRING (12 26, 12 27, 13 27, 13 26, 12 26)", first.getProperties().get("WellKnownText")); + assertEquals("LINEARRING (2 3, 2 5, 6 5, 6 3, 2 3)", second.getProperties().get("WellKnownText")); + assertEquals((Double) first.getProperties().get("Length"), 4.0, 0); + assertEquals((Double) second.getProperties().get("Length"), 12.0, 0); + pipeline.reset(); + } + + /** + * Buffer + *

+ * The Buffer pipe applies a buffer to geometries. + *

+ * Example: + * + * @@s_buffer Output: + * @@buffer + */ + @Documented("get_buffer") + @Test + public void get_buffer() { + // tag::buffer[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBuffer(0.5); + // end::buffer[] + addImageSnippet(boxesLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, boxesLayer).toBuffer(0.1).createWellKnownText().calculateArea().sort("Area"); + + assertTrue(((Double) pipeline.next().getProperties().get("Area")) > 1); + assertTrue(((Double) pipeline.next().getProperties().get("Area")) > 8); + pipeline.reset(); + } + + /** + * Centroid + *

+ * The Centroid pipe calculates geometry centroid. + *

+ * Example: + * + * @@s_centroid Output: + * @@centroid + */ + @Documented("get_centroid") + @Test + public void get_centroid() { + // tag::centroid[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toCentroid(); + // end::centroid[] + addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); + + pipeline = GeoPipeline.start(tx, boxesLayer).toCentroid().createWellKnownText().copyDatabaseRecordProperties(tx) + .sort("name"); + + assertEquals("POINT (12.5 26.5)", pipeline.next().getProperties().get("WellKnownText")); + assertEquals("POINT (4 4)", pipeline.next().getProperties().get("WellKnownText")); + pipeline.reset(); + } + + /** + * This pipe exports every geometry as a + * http://en.wikipedia.org/wiki/Geography_Markup_Language[GML] snippet. + *

+ * Example: + * + * @@s_export_to_gml Output: + * @@exportgml + */ + @Documented("export_to_GML") + @Test + public void export_to_GML() { + // tag::export_to_gml[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).createGML(); + for (GeoPipeFlow flow : pipeline) { + System.out.println(flow.getProperties().get("GML")); + } + // end::export_to_gml[] + String result = ""; + for (GeoPipeFlow flow : GeoPipeline.start(tx, boxesLayer).createGML()) { + result = result + flow.getProperties().get("GML"); + } + gen.get().addSnippet("exportgml", "[source,xml]\n----\n" + result + "\n----\n"); + } + + /** + * Convex Hull + *

+ * The ConvexHull pipe calculates geometry convex hull. + *

+ * Example: + * + * @@s_convex_hull Output: + * @@convex_hull + */ + @Documented("get_convex_hull") + @Test + public void get_convex_hull() { + // tag::convex_hull[] + GeoPipeline pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull(); + // end::convex_hull[] + addImageSnippet(concaveLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull().createWellKnownText(); + + assertEquals("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))", pipeline.next().getProperties().get("WellKnownText")); + pipeline.reset(); + } + + /** + * Densify + *

+ * The Densify pipe inserts extra vertices along the line segments in the geometry. + * The densified geometry contains no line segment which is longer than the given distance tolerance. + *

+ * Example: + * + * @@s_densify Output: + * @@densify + */ + @Documented("densify") + @Test + public void densify() { + // tag::densify[] + GeoPipeline pipeline = GeoPipeline.start(tx, concaveLayer).densify(5).extractPoints(); + // end::densify[] + addImageSnippet(concaveLayer, pipeline, getTitle(), Constants.GTYPE_POINT); + + pipeline = GeoPipeline.start(tx, concaveLayer).toConvexHull().densify(5).createWellKnownText(); + + String wkt = (String) pipeline.next().getProperties().get("WellKnownText"); + pipeline.reset(); + assertEquals("POLYGON ((0 0, 0 5, 0 10, 5 10, 10 10, 10 5, 10 0, 5 0, 0 0))", wkt); + } + + @Test + public void json() { + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).createJson().copyDatabaseRecordProperties(tx) + .sort("name"); + + assertEquals("{\"type\":\"Polygon\",\"coordinates\":[[[12,26],[12,27],[13,27],[13,26],[12,26]]]}", + pipeline.next().getProperties().get("GeoJSON")); + assertEquals("{\"type\":\"Polygon\",\"coordinates\":[[[2,3],[2,5],[6,5],[6,3],[2,3]]]}", + pipeline.next().getProperties().get("GeoJSON")); + pipeline.reset(); + } + + /** + * Max + *

+ * The Max pipe computes the maximum value of the specified property and + * discard items with a value less than the maximum. + *

+ * Example: + * + * @@s_max Output: + * @@max + */ + @Documented("get_max_area") + @Test + public void get_max_area() { + // tag::max[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) + .calculateArea() + .getMax("Area"); + // end::max[] + addImageSnippet(boxesLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().getMax("Area"); + assertEquals((Double) pipeline.next().getProperties().get("Area"), 8.0, 0); + pipeline.reset(); + } + + /** + * The boundary pipe calculates boundary of every geometry in the pipeline. + *

+ * Example: + * + * @@s_boundary Output: + * @@boundary + */ + @Documented("boundary") + @Test + public void boundary() { + // tag::boundary[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).toBoundary(); + // end::boundary[] + addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_LINESTRING); + + // TODO test? + } + + /** + * Difference + *

+ * The Difference pipe computes a geometry representing the points making + * up item geometry that do not make up the given geometry. + *

+ * Example: + * + * @@s_difference Output: + * @@difference + */ + @Documented("difference.") + @Test + public void difference() throws Exception { + // tag::difference[] + WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); + Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); + GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).difference(geometry); + // end::difference[] + addImageSnippet(intersectionLayer, pipeline, getTitle()); + + // TODO test? + } + + /** + * Intersection + *

+ * The Intersection pipe computes a geometry representing the intersection + * between item geometry and the given geometry. + *

+ * Example: + * + * @@s_intersection Output: + * @@intersection + */ + @Documented("intersection") + @Test + public void intersection() throws Exception { + // tag::intersection[] + WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); + Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); + GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).intersect(geometry); + // end::intersection[] + addImageSnippet(intersectionLayer, pipeline, getTitle()); + + // TODO test? + } + + /** + * Union + *

+ * The Union pipe unites item geometry with a given geometry. + *

+ * Example: + * + * @@s_union Output: + * @@union + */ + @Documented("union") + @Test + public void union() throws Exception { + // tag::union[] + WKTReader reader = new WKTReader(intersectionLayer.getGeometryFactory()); + Geometry geometry = reader.read("POLYGON ((3 3, 3 5, 7 7, 7 3, 3 3))"); + SearchFilter filter = new SearchIntersectWindow(intersectionLayer, new Envelope(7, 10, 7, 10)); + GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer, filter).union(geometry); + // end::union[] + addImageSnippet(intersectionLayer, pipeline, getTitle()); + + // TODO test? + } + + /** + * Min + *

+ * The Min pipe computes the minimum value of the specified property and + * discard items with a value greater than the minimum. + *

+ * Example: + * + * @@s_min Output: + * @@min + */ + @Documented("get_min_area") + @Test + public void get_min_area() { + // tag::min[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer) + .calculateArea() + .getMin("Area"); + // end::min[] + addImageSnippet(boxesLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, boxesLayer).calculateArea().getMin("Area"); + assertEquals((Double) pipeline.next().getProperties().get("Area"), 1.0, 0); + pipeline.reset(); + } + + @Test + public void extract_osm_points() { + int count = 0; + GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().createWellKnownText(); + for (GeoPipeFlow flow : pipeline) { + count++; + + assertEquals(1, flow.getProperties().size()); + String wkt = (String) flow.getProperties().get("WellKnownText"); + assertEquals(0, wkt.indexOf("POINT")); + } + + assertEquals(24, count); + } + + /** + * A more complex Open Street Map example. + *

+ * This example demostrates the some pipes chained together to make a full + * geoprocessing pipeline. + *

+ * Example: + * + * @@s_break_up_all_geometries_into_points_and_make_density_islands _Step1_ + * @@step1_break_up_all_geometries_into_points_and_make_density_islands _Step2_ + * @@step2_break_up_all_geometries_into_points_and_make_density_islands _Step3_ + * @@step3_break_up_all_geometries_into_points_and_make_density_islands _Step4_ + * @@step4_break_up_all_geometries_into_points_and_make_density_islands _Step5_ + * @@step5_break_up_all_geometries_into_points_and_make_density_islands + */ + @Documented("break_up_all_geometries_into_points_and_make_density_islands_and_get_the_outer_linear_ring_of_the_density_islands_and_buffer_the_geometry_and_count_them") + @Title("break_up_all_geometries_into_points_and_make_density_islands") + @Test + public void break_up_all_geometries_into_points_and_make_density_islands_and_get_the_outer_linear_ring_of_the_density_islands_and_buffer_the_geometry_and_count_them() { + // tag::break_up_all_geometries_into_points_and_make_density_islands[] + //step1 + GeoPipeline pipeline = OSMGeoPipeline.startOsm(tx, osmLayer) + //step2 + .extractOsmPoints() + //step3 + .groupByDensityIslands(0.0005) + //step4 + .toConvexHull() + //step5 + .toBuffer(0.0004); + // end::break_up_all_geometries_into_points_and_make_density_islands[] + + assertEquals(9, pipeline.count()); + + addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer), "step1_" + getTitle(), + Constants.GTYPE_LINESTRING); + addOsmImageSnippet(osmLayer, OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints(), "step2_" + getTitle(), + Constants.GTYPE_POINT); + addOsmImageSnippet(osmLayer, + OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005), + "step3_" + getTitle(), Constants.GTYPE_POLYGON); + addOsmImageSnippet(osmLayer, + OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005).toConvexHull(), + "step4_" + getTitle(), Constants.GTYPE_POLYGON); + addOsmImageSnippet(osmLayer, + OSMGeoPipeline.startOsm(tx, osmLayer).extractOsmPoints().groupByDensityIslands(0.0005).toConvexHull() + .toBuffer(0.0004), "step5_" + getTitle(), Constants.GTYPE_POLYGON); + } + + /** + * Extract Points + *

+ * The Extract Points pipe extracts every point from a geometry. + *

+ * Example: + * + * @@s_extract_points Output: + * @@extract_points + */ + @Documented("extract_points") + @Test + public void extract_points() { + // tag::extract_points[] + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).extractPoints(); + // end::extract_points[] + addImageSnippet(boxesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); + + int count = 0; + for (GeoPipeFlow flow : GeoPipeline.start(tx, boxesLayer).extractPoints().createWellKnownText()) { + count++; + + assertEquals(1, flow.getProperties().size()); + String wkt = (String) flow.getProperties().get("WellKnownText"); + assertTrue(wkt.indexOf("POINT") == 0); + } + + // every rectangle has 5 points, the last point is in the same position of the first + assertEquals(10, count); + } + + @Test + public void filter_by_null_property() { + assertEquals(2, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNullFilter("address") + .count()); + assertEquals(0, + GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNullFilter("name").count()); + } + + @Test + public void filter_by_not_null_property() { + assertEquals(0, + GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNotNullFilter("address") + .count()); + assertEquals(2, GeoPipeline.start(tx, boxesLayer).copyDatabaseRecordProperties(tx).propertyNotNullFilter("name") + .count()); + } + + @Test + public void compute_distance() throws ParseException { + WKTReader reader = new WKTReader(boxesLayer.getGeometryFactory()); + + GeoPipeline pipeline = GeoPipeline.start(tx, boxesLayer).calculateDistance( + reader.read("POINT (0 0)")).sort("Distance"); + + assertEquals(4, Math.round((Double) pipeline.next().getProperty(tx, "Distance"))); + assertEquals(29, Math.round((Double) pipeline.next().getProperty(tx, "Distance"))); + pipeline.reset(); + } + + /** + * Unite All + *

+ * The Union All pipe unites geometries of every item contained in the pipeline. + * This pipe groups every item in the pipeline in a single item containing the geometry output + * of the union. + *

+ * Example: + * + * @@s_unite_all Output: + * @@unite_all + */ + @Documented("unite_all") + @Test + public void unite_all() { + // tag::unite_all[] + GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).unionAll(); + // end::unite_all[] + addImageSnippet(intersectionLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, intersectionLayer) + .unionAll() + .createWellKnownText(); + + assertEquals("POLYGON ((2 5, 2 6, 4 6, 4 10, 10 10, 10 4, 6 4, 6 2, 5 2, 5 0, 0 0, 0 5, 2 5))", + pipeline.next().getProperty(tx, "WellKnownText")); + + try { + pipeline.next(); + fail(); + } catch (NoSuchElementException ignored) { + } + } + + /** + * Intersect All + *

+ * The IntersectAll pipe intersects geometries of every item contained in the pipeline. + * It groups every item in the pipeline in a single item containing the geometry output + * of the intersection. + *

+ * Example: + * + * @@s_intersect_all Output: + * @@intersect_all + */ + @Documented("intersect_all") + @Test + public void intersect_all() { + // tag::intersect_all[] + GeoPipeline pipeline = GeoPipeline.start(tx, intersectionLayer).intersectAll(); + // end::intersect_all[] + addImageSnippet(intersectionLayer, pipeline, getTitle()); + + pipeline = GeoPipeline.start(tx, intersectionLayer) + .intersectAll() + .createWellKnownText(); + + assertEquals("POLYGON ((4 5, 5 5, 5 4, 4 4, 4 5))", pipeline.next().getProperty(tx, "WellKnownText")); + + try { + pipeline.next(); + fail(); + } catch (NoSuchElementException ignored) { + } + } + + /** + * Intersecting windows + *

+ * The FilterIntersectWindow pipe finds geometries that intersects a given rectangle. + * This pipeline: + * + * @@s_intersecting_windows will output: + * @@intersecting_windows + */ + @Documented("intersecting_windows") + @Test + public void intersecting_windows() { + // tag::intersecting_windows[] + GeoPipeline pipeline = GeoPipeline + .start(tx, boxesLayer) + .windowIntersectionFilter(new Envelope(0, 10, 0, 10)); + // end::intersecting_windows[] + addImageSnippet(boxesLayer, pipeline, getTitle()); + + // TODO test? + } + + /** + * Start Point + *

+ * The StartPoint pipe finds the starting point of item geometry. + *

+ * Example: + * + * @@s_start_point Output: + * @@start_point + */ + @Documented("start_point") + @Test + public void start_point() { + // tag::start_point[] + GeoPipeline pipeline = GeoPipeline + .start(tx, linesLayer) + .toStartPoint(); + // end::start_point[] + addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); + + pipeline = GeoPipeline + .start(tx, linesLayer) + .toStartPoint() + .createWellKnownText(); + + assertEquals("POINT (12 26)", pipeline.next().getProperty(tx, "WellKnownText")); + pipeline.reset(); + } + + /** + * End Point + *

+ * The EndPoint pipe finds the ending point of item geometry. + *

+ * Example: + * + * @@s_end_point Output: + * @@end_point + */ + @Documented("end_point") + @Test + public void end_point() { + // tag::end_point[] + GeoPipeline pipeline = GeoPipeline + .start(tx, linesLayer) + .toEndPoint(); + // end::end_point[] + addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POINT); + + pipeline = GeoPipeline + .start(tx, linesLayer) + .toEndPoint() + .createWellKnownText(); + + assertEquals("POINT (23 34)", pipeline.next().getProperty(tx, "WellKnownText")); + pipeline.reset(); + } + + /** + * Envelope + *

+ * The Envelope pipe computes the minimum bounding box of item geometry. + *

+ * Example: + * + * @@s_envelope Output: + * @@envelope + */ + @Documented("envelope") + @Test + public void envelope() { + // tag::envelope[] + GeoPipeline pipeline = GeoPipeline + .start(tx, linesLayer) + .toEnvelope(); + // end::envelope[] + addImageSnippet(linesLayer, pipeline, getTitle(), Constants.GTYPE_POLYGON); + + // TODO test + } + + @Test + public void test_equality() throws Exception { + WKTReader reader = new WKTReader(equalLayer.getGeometryFactory()); + Geometry geom = reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))"); + + GeoPipeline pipeline = GeoPipeline.startEqualExactSearch(tx, equalLayer, geom, 0) + .copyDatabaseRecordProperties(tx); + assertEquals("equal", pipeline.next().getProperty(tx, "name")); + assertFalse(pipeline.hasNext()); + + pipeline = GeoPipeline.startEqualExactSearch(tx, equalLayer, geom, 0.1).copyDatabaseRecordProperties(tx) + .sort("id"); + assertEquals("equal", pipeline.next().getProperty(tx, "name")); + assertEquals("tolerance", pipeline.next().getProperty(tx, "name")); + assertFalse(pipeline.hasNext()); + + pipeline = GeoPipeline.startIntersectWindowSearch(tx, equalLayer, + geom.getEnvelopeInternal()).equalNormFilter(geom, 0.1).copyDatabaseRecordProperties(tx).sort("id"); + assertEquals("equal", pipeline.next().getProperty(tx, "name")); + assertEquals("tolerance", pipeline.next().getProperty(tx, "name")); + assertEquals("different order", pipeline.next().getProperty(tx, "name")); + assertFalse(pipeline.hasNext()); + + pipeline = GeoPipeline.startIntersectWindowSearch(tx, equalLayer, + geom.getEnvelopeInternal()).equalTopoFilter(geom).copyDatabaseRecordProperties(tx).sort("id"); + assertEquals("equal", pipeline.next().getProperty(tx, "name")); + assertEquals("different order", pipeline.next().getProperty(tx, "name")); + assertEquals("topo equal", pipeline.next().getProperty(tx, "name")); + assertFalse(pipeline.hasNext()); + pipeline.reset(); + } + + private String getTitle() { + return gen.get().getTitle().replace(" ", "_").toLowerCase(); + } + + private void addImageSnippet( + Layer layer, + GeoPipeline pipeline, + String imgName) { + addImageSnippet(layer, pipeline, imgName, null); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void addOsmImageSnippet( + Layer layer, + GeoPipeline pipeline, + String imgName, + Integer geomType) { + addImageSnippet(layer, pipeline, imgName, geomType, 0.002); + } + + private void addImageSnippet( + Layer layer, + GeoPipeline pipeline, + String imgName, + Integer geomType) { + addImageSnippet(layer, pipeline, imgName, geomType, 1); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void addImageSnippet( + Layer layer, + GeoPipeline pipeline, + String imgName, + Integer geomType, + double boundsDelta) { + gen.get().addSnippet(imgName, "\nimage::" + imgName + ".png[scaledwidth=\"75%\"]\n"); + + try { + FeatureCollection layerCollection = GeoPipeline.start(tx, layer, new SearchAll()).toFeatureCollection(tx); + FeatureCollection pipelineCollection; + if (geomType == null) { + pipelineCollection = pipeline.toFeatureCollection(tx); + } else { + pipelineCollection = pipeline.toFeatureCollection(tx, + Neo4jFeatureBuilder.getType(layer.getName(), geomType, layer.getCoordinateReferenceSystem(tx), + layer.getExtraPropertyNames(tx))); + } + + ReferencedEnvelope bounds = layerCollection.getBounds(); + bounds.expandToInclude(pipelineCollection.getBounds()); + bounds.expandBy(boundsDelta, boundsDelta); + + StyledImageExporter exporter = new StyledImageExporter(db); + exporter.setExportDir("target/docs/images/"); + exporter.saveImage( + new FeatureCollection[]{ + layerCollection, + pipelineCollection, + }, + new Style[]{ + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), + StyledImageExporter.createDefaultStyle(Color.RED, Color.ORANGE) + }, + new File(imgName + ".png"), + bounds); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void load() throws Exception { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) db, SecurityContext.AUTH_DISABLED)); + + try (Transaction tx = db.beginTx()) { + loadTestOsmData("two-street.osm", 100); + osmLayer = spatial.getLayer(tx, "two-street.osm"); + + boxesLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "boxes"); + boxesLayer.setExtraPropertyNames(new String[]{"name"}, tx); + boxesLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); + WKTReader reader = new WKTReader(boxesLayer.getGeometryFactory()); + boxesLayer.add(tx, + reader.read("POLYGON ((12 26, 12 27, 13 27, 13 26, 12 26))"), + new String[]{"name"}, new Object[]{"A"}); + boxesLayer.add(tx, + reader.read("POLYGON ((2 3, 2 5, 6 5, 6 3, 2 3))"), + new String[]{"name"}, new Object[]{"B"}); + + concaveLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "concave"); + concaveLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); + reader = new WKTReader(concaveLayer.getGeometryFactory()); + concaveLayer.add(tx, reader.read("POLYGON ((0 0, 2 5, 0 10, 10 10, 10 0, 0 0))")); + + intersectionLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "intersection"); + intersectionLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); + reader = new WKTReader(intersectionLayer.getGeometryFactory()); + intersectionLayer.add(tx, reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))")); + intersectionLayer.add(tx, reader.read("POLYGON ((4 4, 4 10, 10 10, 10 4, 4 4))")); + intersectionLayer.add(tx, reader.read("POLYGON ((2 2, 2 6, 6 6, 6 2, 2 2))")); + + equalLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "equal"); + equalLayer.setExtraPropertyNames(new String[]{"id", "name"}, tx); + equalLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); + reader = new WKTReader(intersectionLayer.getGeometryFactory()); + equalLayer.add(tx, reader.read("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))"), + new String[]{"id", "name"}, new Object[]{1, "equal"}); + equalLayer.add(tx, reader.read("POLYGON ((0 0, 0.1 5, 5 5, 5 0, 0 0))"), + new String[]{"id", "name"}, new Object[]{2, "tolerance"}); + equalLayer.add(tx, reader.read("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 5))"), + new String[]{"id", "name"}, new Object[]{3, + "different order"}); + equalLayer.add(tx, + reader.read("POLYGON ((0 0, 0 2, 0 4, 0 5, 5 5, 5 3, 5 2, 5 0, 0 0))"), + new String[]{"id", "name"}, new Object[]{4, "topo equal"}); + + linesLayer = (EditableLayerImpl) spatial.getOrCreateEditableLayer(tx, "lines"); + linesLayer.setCoordinateReferenceSystem(tx, DefaultEngineeringCRS.GENERIC_2D); + reader = new WKTReader(intersectionLayer.getGeometryFactory()); + linesLayer.add(tx, reader.read("LINESTRING (12 26, 15 27, 18 32, 20 38, 23 34)")); + + tx.commit(); + } + } + + @SuppressWarnings("SameParameterValue") + private static void loadTestOsmData(String layerName, int commitInterval) + throws Exception { + String osmPath = "./" + layerName; + System.out.println("\n=== Loading layer " + layerName + " from " + + osmPath + " ==="); + OSMImporter importer = new OSMImporter(layerName); + importer.setCharset(StandardCharsets.UTF_8); + importer.importFile(db, osmPath); + importer.reIndex(db, commitInterval); + } + + @BeforeEach + public void setUp() { + gen.get().setGraph(db); + try (Transaction tx = db.beginTx()) { + StyledImageExporter exporter = new StyledImageExporter(db); + exporter.setExportDir("target/docs/images/"); + exporter.saveImage(GeoPipeline.start(tx, intersectionLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "intersectionLayer.png")); + + exporter.saveImage(GeoPipeline.start(tx, boxesLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "boxesLayer.png")); + + exporter.saveImage(GeoPipeline.start(tx, concaveLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "concaveLayer.png")); + + exporter.saveImage(GeoPipeline.start(tx, equalLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "equalLayer.png")); + exporter.saveImage(GeoPipeline.start(tx, linesLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "linesLayer.png")); + exporter.saveImage(GeoPipeline.start(tx, osmLayer).toFeatureCollection(tx), + StyledImageExporter.createDefaultStyle(Color.BLUE, Color.CYAN), new File( + "osmLayer.png")); + tx.commit(); + } catch (IOException e) { + e.printStackTrace(); + } + tx = db.beginTx(); + } + + @AfterEach + public void doc() { + // gen.get().addSnippet( "graph", AsciidocHelper.createGraphViz( imgName , graphdb(), "graph"+getTitle() ) ); + gen.get().addTestSourceSnippets(GeoPipesDocTest.class, "s_" + getTitle().toLowerCase()); + gen.get().document("target/docs", "examples"); + if (tx != null) { + tx.commit(); + tx.close(); + } + } + + @BeforeAll + public static void init() { + databases = new TestDatabaseManagementServiceBuilder(new File("target/docs").toPath()).impermanent().build(); + db = databases.database(DEFAULT_DATABASE_NAME); + try { + load(); + } catch (Exception e) { + e.printStackTrace(); + } + + StyledImageExporter exporter = new StyledImageExporter(db); + exporter.setExportDir("target/docs/images/"); + } + + private GeoPipeFlow print(GeoPipeFlow pipeFlow) { + System.out.println("GeoPipeFlow:"); + for (String key : pipeFlow.getProperties().keySet()) { + System.out.println(key + "=" + pipeFlow.getProperties().get(key)); + } + System.out.println("-"); + return pipeFlow; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesPerformanceTest.java b/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesPerformanceTest.java index f855cb826..176e43131 100644 --- a/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesPerformanceTest.java +++ b/src/test/java/org/neo4j/gis/spatial/pipes/GeoPipesPerformanceTest.java @@ -19,163 +19,175 @@ */ package org.neo4j.gis.spatial.pipes; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; -import org.neo4j.gis.spatial.*; +import org.neo4j.gis.spatial.Layer; +import org.neo4j.gis.spatial.Neo4jTestCase; +import org.neo4j.gis.spatial.SimplePointLayer; +import org.neo4j.gis.spatial.SpatialDatabaseRecord; +import org.neo4j.gis.spatial.SpatialDatabaseService; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.graphdb.Transaction; import org.neo4j.internal.kernel.api.security.SecurityContext; import org.neo4j.kernel.internal.GraphDatabaseAPI; -import java.util.ArrayList; +public class GeoPipesPerformanceTest extends Neo4jTestCase { -import static org.junit.Assert.assertTrue; + private int records = 10000; + private int chunkSize = records / 10; -public class GeoPipesPerformanceTest extends Neo4jTestCase { - private int records = 10000; - private int chunkSize = records / 10; + @BeforeEach + public void setUp() throws Exception { + super.setUp(true); + loadSamplePointData(); + } - @BeforeEach - public void setUp() throws Exception { - super.setUp(true); - loadSamplePointData(); - } + private void loadSamplePointData() { + try (Transaction tx = graphDb().beginTx()) { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + SimplePointLayer layer = spatial.createSimplePointLayer(tx, "GeoPipesPerformanceTest"); + System.out.println("Creating database of " + records + " point records"); + for (int i = 0; i < records; i++) { + double x = 10.0 + Math.random() * 10.0; + double y = 10.0 + Math.random() * 10.0; + String name = "Fake Geometry " + i; + // System.out.println("Creating point '" + name + + // "' at location x:" + x + " y:" + y); + SpatialDatabaseRecord record = layer.add(tx, x, y); + record.getGeomNode().setProperty("name", name); + } + tx.commit(); + System.out.println("Finished writing " + records + " point records to database"); + } catch (Exception e) { + System.err.println("Error initializing database: " + e); + } + } - private void loadSamplePointData() { - try (Transaction tx = graphDb().beginTx()) { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - SimplePointLayer layer = spatial.createSimplePointLayer(tx, "GeoPipesPerformanceTest"); - System.out.println("Creating database of " + records + " point records"); - for (int i = 0; i < records; i++) { - double x = 10.0 + Math.random() * 10.0; - double y = 10.0 + Math.random() * 10.0; - String name = "Fake Geometry " + i; - // System.out.println("Creating point '" + name + - // "' at location x:" + x + " y:" + y); - SpatialDatabaseRecord record = layer.add(tx, x, y); - record.getGeomNode().setProperty("name", name); - } - tx.commit(); - System.out.println("Finished writing " + records + " point records to database"); - } catch (Exception e) { - System.err.println("Error initializing database: " + e); - } - } + class TimeRecord { - class TimeRecord { - int chunk; - int time; - int count; + int chunk; + int time; + int count; - TimeRecord(int chunk, int time, int count) { - this.chunk = chunk; - this.time = time; - this.count = count; - } + TimeRecord(int chunk, int time, int count) { + this.chunk = chunk; + this.time = time; + this.count = count; + } - public float average() { - if (count > 0) { - return (float) time / (float) count; - } else { - return 0; - } - } + public float average() { + if (count > 0) { + return (float) time / (float) count; + } else { + return 0; + } + } - public String toString() { - if (count > 0) { - return "" + chunk + ": " + average() + "ms per record (" + count + " records over " + time + "ms)"; - } else { - return "" + chunk + ": INVALID (" + count + " records over " + time + "ms)"; - } - } - } + public String toString() { + if (count > 0) { + return "" + chunk + ": " + average() + "ms per record (" + count + " records over " + time + "ms)"; + } else { + return "" + chunk + ": INVALID (" + count + " records over " + time + "ms)"; + } + } + } - @Test - public void testQueryPerformance() { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, "GeoPipesPerformanceTest"); - // String[] keys = {"id","name","address","city","state","zip"}; - String[] keys = {"id", "name"}; - Coordinate loc = new Coordinate(15.0, 15.0); - GeoPipeline flowList = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, loc, records).copyDatabaseRecordProperties(tx, keys); - int i = 0; - ArrayList totals = new ArrayList(); - long prevTime = System.currentTimeMillis(); - long prevChunk = 0; - while (flowList.hasNext()) { - GeoPipeFlow geoPipeFlow = flowList.next(); - // System.out.println("Result: " + geoPipeFlow.countRecords() + - // " records"); - int chunk = i / chunkSize; - if (chunk != prevChunk) { - long time = System.currentTimeMillis(); - totals.add(new TimeRecord(chunk, (int) (time - prevTime), chunkSize)); - prevTime = time; - prevChunk = chunk; - } - i++; - } - if (i % chunkSize > 0) { - totals.add(new TimeRecord(totals.size(), (int) (System.currentTimeMillis() - prevTime), i % chunkSize)); - } - int total = 0; - int count = 0; - System.out.println("Measured " + totals.size() + " groups of reads of up to " + chunkSize + " records"); - for (TimeRecord rec : totals) { - total += rec.time; - count += rec.count; - System.out.println("\t" + rec); - float average = (float) rec.time / (float) rec.count; - assertTrue("Expected record average of " + rec.average() + " to not be substantially larger than running average " - + average, rec.average() < 2 * average); - } - tx.commit(); - } - } + @Test + public void testQueryPerformance() { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, "GeoPipesPerformanceTest"); + // String[] keys = {"id","name","address","city","state","zip"}; + String[] keys = {"id", "name"}; + Coordinate loc = new Coordinate(15.0, 15.0); + GeoPipeline flowList = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, loc, records) + .copyDatabaseRecordProperties(tx, keys); + int i = 0; + ArrayList totals = new ArrayList(); + long prevTime = System.currentTimeMillis(); + long prevChunk = 0; + while (flowList.hasNext()) { + GeoPipeFlow geoPipeFlow = flowList.next(); + // System.out.println("Result: " + geoPipeFlow.countRecords() + + // " records"); + int chunk = i / chunkSize; + if (chunk != prevChunk) { + long time = System.currentTimeMillis(); + totals.add(new TimeRecord(chunk, (int) (time - prevTime), chunkSize)); + prevTime = time; + prevChunk = chunk; + } + i++; + } + if (i % chunkSize > 0) { + totals.add(new TimeRecord(totals.size(), (int) (System.currentTimeMillis() - prevTime), i % chunkSize)); + } + int total = 0; + int count = 0; + System.out.println("Measured " + totals.size() + " groups of reads of up to " + chunkSize + " records"); + for (TimeRecord rec : totals) { + total += rec.time; + count += rec.count; + System.out.println("\t" + rec); + float average = (float) rec.time / (float) rec.count; + assertTrue("Expected record average of " + rec.average() + + " to not be substantially larger than running average " + + average, rec.average() < 2 * average); + } + tx.commit(); + } + } - @Test - public void testPagingPerformance() { - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); - try (Transaction tx = graphDb().beginTx()) { - Layer layer = spatial.getLayer(tx, "GeoPipesPerformanceTest"); - // String[] keys = {"id","name","address","city","state","zip"}; - String[] keys = {"id", "name"}; - Coordinate loc = new Coordinate(15.0, 15.0); - ArrayList totals = new ArrayList(); - long prevTime = System.currentTimeMillis(); - for (int chunk = 0; chunk < 20; chunk++) { - int low = chunk * chunkSize; - int high = (chunk + 1) * chunkSize - 1; - GeoPipeline flowList = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, loc, records).range(low, high).copyDatabaseRecordProperties(tx, keys); - if (!flowList.hasNext()) - break; - int count = 0; - while (flowList.hasNext()) { - GeoPipeFlow geoPipeFlow = flowList.next(); - // System.out.println("Result: " + geoPipeFlow.countRecords() + - // " records"); - count++; - } - flowList.reset(); - long time = System.currentTimeMillis(); - totals.add(new TimeRecord(chunk, (int) (time - prevTime), count)); - prevTime = time; - } - int total = 0; - int count = 0; - System.out.println("Measured " + totals.size() + " groups of reads of up to " + chunkSize + " records"); - for (TimeRecord rec : totals) { - total += rec.time; - count += rec.count; - System.out.println("\t" + rec); - float average = (float) rec.time / (float) rec.count; - // assertTrue("Expected record average of " + rec.average() + - // " to not be substantially larger than running average " - // + average, rec.average() < 2 * average); - } - tx.commit(); - } - } + @Test + public void testPagingPerformance() { + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) graphDb(), SecurityContext.AUTH_DISABLED)); + try (Transaction tx = graphDb().beginTx()) { + Layer layer = spatial.getLayer(tx, "GeoPipesPerformanceTest"); + // String[] keys = {"id","name","address","city","state","zip"}; + String[] keys = {"id", "name"}; + Coordinate loc = new Coordinate(15.0, 15.0); + ArrayList totals = new ArrayList(); + long prevTime = System.currentTimeMillis(); + for (int chunk = 0; chunk < 20; chunk++) { + int low = chunk * chunkSize; + int high = (chunk + 1) * chunkSize - 1; + GeoPipeline flowList = GeoPipeline.startNearestNeighborLatLonSearch(tx, layer, loc, records) + .range(low, high).copyDatabaseRecordProperties(tx, keys); + if (!flowList.hasNext()) { + break; + } + int count = 0; + while (flowList.hasNext()) { + GeoPipeFlow geoPipeFlow = flowList.next(); + // System.out.println("Result: " + geoPipeFlow.countRecords() + + // " records"); + count++; + } + flowList.reset(); + long time = System.currentTimeMillis(); + totals.add(new TimeRecord(chunk, (int) (time - prevTime), count)); + prevTime = time; + } + int total = 0; + int count = 0; + System.out.println("Measured " + totals.size() + " groups of reads of up to " + chunkSize + " records"); + for (TimeRecord rec : totals) { + total += rec.time; + count += rec.count; + System.out.println("\t" + rec); + float average = (float) rec.time / (float) rec.count; + // assertTrue("Expected record average of " + rec.average() + + // " to not be substantially larger than running average " + // + average, rec.average() < 2 * average); + } + tx.commit(); + } + } } diff --git a/src/test/java/org/neo4j/gis/spatial/pipes/OrthodromicDistanceTest.java b/src/test/java/org/neo4j/gis/spatial/pipes/OrthodromicDistanceTest.java index 3c32c252e..00165f261 100644 --- a/src/test/java/org/neo4j/gis/spatial/pipes/OrthodromicDistanceTest.java +++ b/src/test/java/org/neo4j/gis/spatial/pipes/OrthodromicDistanceTest.java @@ -19,6 +19,10 @@ */ package org.neo4j.gis.spatial.pipes; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.number.IsCloseTo.closeTo; + import org.junit.jupiter.api.Test; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; @@ -26,95 +30,97 @@ import org.locationtech.jts.geom.Polygon; import org.neo4j.gis.spatial.pipes.processing.OrthodromicDistance; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.number.IsCloseTo.closeTo; - public class OrthodromicDistanceTest { - @Test - public void shouldCalculateDistanceBetweenIdenticalPoints() { - Coordinate pointA = new Coordinate(1.0, 1.0); - Coordinate pointB = new Coordinate(1.0, 1.0); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), equalTo(0.0)); - } + @Test + public void shouldCalculateDistanceBetweenIdenticalPoints() { + Coordinate pointA = new Coordinate(1.0, 1.0); + Coordinate pointB = new Coordinate(1.0, 1.0); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), equalTo(0.0)); + } - @Test - public void shouldCalculateDistanceBetweenSamePoint() { - Coordinate pointA = new Coordinate(1.0, 1.0); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointA), closeTo(0.0, 0.000001)); - } + @Test + public void shouldCalculateDistanceBetweenSamePoint() { + Coordinate pointA = new Coordinate(1.0, 1.0); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointA), closeTo(0.0, 0.000001)); + } - @Test - public void shouldCalculateDistanceBetweenClosePoints() { - Coordinate pointA = new Coordinate(1.0001, 1.0001); - Coordinate pointB = new Coordinate(1.0, 1.0); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.015724, 0.000001)); - } + @Test + public void shouldCalculateDistanceBetweenClosePoints() { + Coordinate pointA = new Coordinate(1.0001, 1.0001); + Coordinate pointB = new Coordinate(1.0, 1.0); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), + closeTo(0.015724, 0.000001)); + } - @Test - public void shouldCalculateDistanceBetweenIdenticalPointsWithManyDecimalPlaces() { - Coordinate pointA = new Coordinate(0.0905302, 52.2029252); - Coordinate pointB = new Coordinate(0.0905302, 52.2029252); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointB, pointA), closeTo(0.0, 0.000001)); - } + @Test + public void shouldCalculateDistanceBetweenIdenticalPointsWithManyDecimalPlaces() { + Coordinate pointA = new Coordinate(0.0905302, 52.2029252); + Coordinate pointB = new Coordinate(0.0905302, 52.2029252); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointB, pointA), closeTo(0.0, 0.000001)); + } - @Test - public void shouldCalculateDistanceBetweenClosePointsDifferingOnlyBySignificantDigits() { - Coordinate pointA = new Coordinate(0.0905302, 52.2029252); - Coordinate pointB = new Coordinate(0.0905302, 52.202925); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointB, pointA), closeTo(0.0, 0.000001)); - } + @Test + public void shouldCalculateDistanceBetweenClosePointsDifferingOnlyBySignificantDigits() { + Coordinate pointA = new Coordinate(0.0905302, 52.2029252); + Coordinate pointB = new Coordinate(0.0905302, 52.202925); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointB, pointA), closeTo(0.0, 0.000001)); + } - @Test - public void shouldCalculateDistanceBetweenDistantPoints() { - Coordinate pointA = new Coordinate(-80.0, 80.0); - Coordinate pointB = new Coordinate(80.0, -80.0); - assertThat("Should be a big number", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(19630.8, 0.1)); - } + @Test + public void shouldCalculateDistanceBetweenDistantPoints() { + Coordinate pointA = new Coordinate(-80.0, 80.0); + Coordinate pointB = new Coordinate(80.0, -80.0); + assertThat("Should be a big number", OrthodromicDistance.calculateDistance(pointA, pointB), + closeTo(19630.8, 0.1)); + } - @Test - public void shouldCalculateDistanceBetweenOppositePointsOnTheWorld() { - Coordinate pointA = new Coordinate(-90.0, 0.0); - Coordinate pointB = new Coordinate(90.0, 0.0); - double halfEarthCircumference = Math.PI * OrthodromicDistance.earthRadiusInKm; - assertThat("Should be half the earths circumference", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(halfEarthCircumference, 0.1)); - } + @Test + public void shouldCalculateDistanceBetweenOppositePointsOnTheWorld() { + Coordinate pointA = new Coordinate(-90.0, 0.0); + Coordinate pointB = new Coordinate(90.0, 0.0); + double halfEarthCircumference = Math.PI * OrthodromicDistance.earthRadiusInKm; + assertThat("Should be half the earths circumference", OrthodromicDistance.calculateDistance(pointA, pointB), + closeTo(halfEarthCircumference, 0.1)); + } - @Test - public void shouldCalculateDistanceBetweenIdenticalPointAroundTheWorld() { - Coordinate pointA = new Coordinate(-180.0, 0.0); - Coordinate pointB = new Coordinate(180.0, 0.0); - assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); - } + @Test + public void shouldCalculateDistanceBetweenIdenticalPointAroundTheWorld() { + Coordinate pointA = new Coordinate(-180.0, 0.0); + Coordinate pointB = new Coordinate(180.0, 0.0); + assertThat("Should be zero", OrthodromicDistance.calculateDistance(pointA, pointB), closeTo(0.0, 0.000001)); + } - @Test - public void shouldCalculateDistanceToPolygon() { - GeometryFactory factory = new GeometryFactory(); - Point reference = factory.createPoint(new Coordinate(0, 0)); - Polygon polygon = factory.createPolygon(new Coordinate[]{ - new Coordinate(1, -1), - new Coordinate(1, 1), - new Coordinate(2, 1), - new Coordinate(2, -1), - new Coordinate(1, -1) - }); - assertThat("Should be positive number", OrthodromicDistance.calculateDistanceToGeometry(reference.getCoordinate(), polygon), closeTo(111, 1)); - } + @Test + public void shouldCalculateDistanceToPolygon() { + GeometryFactory factory = new GeometryFactory(); + Point reference = factory.createPoint(new Coordinate(0, 0)); + Polygon polygon = factory.createPolygon(new Coordinate[]{ + new Coordinate(1, -1), + new Coordinate(1, 1), + new Coordinate(2, 1), + new Coordinate(2, -1), + new Coordinate(1, -1) + }); + assertThat("Should be positive number", + OrthodromicDistance.calculateDistanceToGeometry(reference.getCoordinate(), polygon), closeTo(111, 1)); + } - @Test - public void shouldCalculateDistanceToEncompassingPolygon() { - GeometryFactory factory = new GeometryFactory(); - Point reference = factory.createPoint(new Coordinate(0, 0)); - Polygon polygon = factory.createPolygon(new Coordinate[]{ - new Coordinate(1, -1), - new Coordinate(1, 1), - new Coordinate(-1, 1), - new Coordinate(-1, -1), - new Coordinate(1, -1) - }); - assertThat("Should be zero", OrthodromicDistance.calculateDistanceToGeometry(reference.getCoordinate(), polygon), closeTo(0, 0.00001)); - } + @Test + public void shouldCalculateDistanceToEncompassingPolygon() { + GeometryFactory factory = new GeometryFactory(); + Point reference = factory.createPoint(new Coordinate(0, 0)); + Polygon polygon = factory.createPolygon(new Coordinate[]{ + new Coordinate(1, -1), + new Coordinate(1, 1), + new Coordinate(-1, 1), + new Coordinate(-1, -1), + new Coordinate(1, -1) + }); + assertThat("Should be zero", + OrthodromicDistance.calculateDistanceToGeometry(reference.getCoordinate(), polygon), + closeTo(0, 0.00001)); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java b/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java index 8cb4b090b..941ec790f 100644 --- a/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java +++ b/src/test/java/org/neo4j/gis/spatial/procedures/SpatialProceduresTest.java @@ -19,8 +19,38 @@ */ package org.neo4j.gis.spatial.procedures; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; +import static org.neo4j.gis.spatial.Constants.LABEL_LAYER; +import static org.neo4j.gis.spatial.Constants.PROP_GEOMENCODER; +import static org.neo4j.gis.spatial.Constants.PROP_GEOMENCODER_CONFIG; +import static org.neo4j.gis.spatial.Constants.PROP_LAYER; +import static org.neo4j.gis.spatial.Constants.PROP_LAYER_CLASS; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; import org.hamcrest.MatcherAssert; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.dbms.api.DatabaseManagementService; @@ -30,7 +60,11 @@ import org.neo4j.gis.spatial.SpatialRelationshipTypes; import org.neo4j.gis.spatial.index.IndexManager; import org.neo4j.gis.spatial.utilities.ReferenceNodes; -import org.neo4j.graphdb.*; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.ResourceIterator; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.spatial.Geometry; import org.neo4j.graphdb.spatial.Point; import org.neo4j.internal.helpers.collection.Iterators; @@ -41,1135 +75,1290 @@ import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; -import static org.neo4j.gis.spatial.Constants.*; - public class SpatialProceduresTest { - private DatabaseManagementService databases; - private GraphDatabaseService db; - - @Before - public void setUp() throws KernelException, IOException { - Path dbRoot = new File("target/procedures").toPath(); - FileUtils.deleteDirectory(dbRoot); - databases = new TestDatabaseManagementServiceBuilder(dbRoot) - .setConfig(GraphDatabaseSettings.procedure_unrestricted, List.of("spatial.*")) - .setConfig(GraphDatabaseInternalSettings.trace_cursors, true) - .impermanent().build(); - db = databases.database(DEFAULT_DATABASE_NAME); - registerProceduresAndFunctions(db, SpatialProcedures.class); - } - - @After - public void tearDown() { - databases.shutdown(); - } - - public static void testCall(GraphDatabaseService db, String call, Consumer> consumer) { - testCall(db, call, null, consumer); - } - - public static Map map(Object... values) { - Map map = new LinkedHashMap<>(); - for (int i = 0; i < values.length; i += 2) { - map.put(values[i].toString(), values[i + 1]); - } - return map; - } - - public static void testCall(GraphDatabaseService db, String call, Map params, Consumer> consumer) { - testCall(db, call, params, consumer, true); - } - - public static void testCallFails(GraphDatabaseService db, String call, Map params, String error) { - try { - testResult(db, call, params, (res) -> { - while (res.hasNext()) { - res.next(); - } - }); - fail("Expected an exception containing '" + error + "', but no exception was thrown"); - } catch (Exception e) { - assertTrue(e.getMessage().contains(error)); - } - } - - public static void testCall(GraphDatabaseService db, String call, Map params, Consumer> consumer, boolean onlyOne) { - testResult(db, call, params, (res) -> { - Assert.assertTrue("Expect at least one result but got none: " + call, res.hasNext()); - Map row = res.next(); - consumer.accept(row); - if (onlyOne) { - Assert.assertFalse("Expected only one result, but there are more", res.hasNext()); - } - }); - } - - public static void testCallCount(GraphDatabaseService db, String call, Map params, int count) { - testResult(db, call, params, (res) -> { - int numLeft = count; - while (numLeft > 0) { - assertTrue("Expected " + count + " results but found only " + (count - numLeft), res.hasNext()); - res.next(); - numLeft--; - } - Assert.assertFalse("Expected " + count + " results but there are more", res.hasNext()); - }); - } - - public static void testResult(GraphDatabaseService db, String call, Consumer resultConsumer) { - testResult(db, call, null, resultConsumer); - } - - public static void testResult(GraphDatabaseService db, String call, Map params, Consumer resultConsumer) { - try (Transaction tx = db.beginTx()) { - Map p = (params == null) ? map() : params; - resultConsumer.accept(tx.execute(call, p)); - tx.commit(); - } - } - - public static void registerProceduresAndFunctions(GraphDatabaseService db, Class procedure) throws KernelException { - GlobalProcedures procedures = ((GraphDatabaseAPI) db).getDependencyResolver().resolveDependency(GlobalProcedures.class); - procedures.registerProcedure(procedure); - procedures.registerFunction(procedure); - } - - private Layer makeLayerOfVariousTypes(SpatialDatabaseService spatial, Transaction tx, String name, int index) { - switch (index % 3) { - case 0: - return spatial.getOrCreateSimplePointLayer(tx, name, SpatialDatabaseService.RTREE_INDEX_NAME, "x", "y"); - case 1: - return spatial.getOrCreateNativePointLayer(tx, name, SpatialDatabaseService.RTREE_INDEX_NAME, "location"); - default: - return spatial.getOrCreateDefaultLayer(tx, name); - } - } - - private void makeOldSpatialModel(Transaction tx, String... layers) { - KernelTransaction ktx = ((InternalTransaction) tx).kernelTransaction(); - SpatialDatabaseService spatial = new SpatialDatabaseService(new IndexManager((GraphDatabaseAPI) db, ktx.securityContext())); - ArrayList layerNodes = new ArrayList<>(); - int index = 0; - // First create a set of layers - for (String name : layers) { - Layer layer = makeLayerOfVariousTypes(spatial, tx, name, index); - layerNodes.add(layer.getLayerNode(tx)); - index++; - } - // Then downgrade to old format, without label and with reference node and relationships - Node root = ReferenceNodes.createDeprecatedReferenceNode(tx, "spatial_root"); - for (Node node : layerNodes) { - node.removeLabel(LABEL_LAYER); - root.createRelationshipTo(node, SpatialRelationshipTypes.LAYER); - } - } - - @Test - public void old_spatial_model_throws_errors() { - try (Transaction tx = db.beginTx()) { - makeOldSpatialModel(tx, "layer1", "layer2", "layer3"); - tx.commit(); - } - testCallFails(db, "CALL spatial.layers", null, "Old reference node exists - please upgrade the spatial database to the new format"); - } - - @Test - public void old_spatial_model_can_be_upgraded() { - try (Transaction tx = db.beginTx()) { - makeOldSpatialModel(tx, "layer1", "layer2", "layer3"); - tx.commit(); - } - testCallFails(db, "CALL spatial.layers", null, "Old reference node exists - please upgrade the spatial database to the new format"); - testCallCount(db, "CALL spatial.upgrade", null, 3); - testCallCount(db, "CALL spatial.layers", null, 3); - } - @Test - public void add_node_to_non_existing_layer() { - execute("CALL spatial.addPointLayer('some_name')"); - Node node = createNode("CREATE (n:Point {latitude:60.1,longitude:15.2}) RETURN n", "n"); - testCallFails(db, "CALL spatial.addNode.byId('wrong_name',$nodeId)", map("nodeId", node.getElementId()), "No such layer 'wrong_name'"); - } - - @Test - public void add_node_point_layer() { - execute("CALL spatial.addPointLayer('points')"); - executeWrite("CREATE (n:Point {latitude:60.1,longitude:15.2})"); - Node node = createNode("MATCH (n:Point) WITH n CALL spatial.addNode('points',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('points',{longitude:15.0,latitude:60.0},{longitude:15.3, latitude:60.2})", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('points',{longitude:15.0,latitude:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void add_node_and_search_bbox_and_distance() { - execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); - Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - // This tests issue https://github.com/neo4j-contrib/spatial/issues/298 - public void add_node_point_layer_and_search_multiple_points_precision() { - execute("CALL spatial.addPointLayer('bar')"); - execute("create (n:Point) set n={latitude: 52.2029252, longitude: 0.0905302} with n call spatial.addNode('bar', n) yield node return node"); - execute("create (n:Point) set n={latitude: 52.202925, longitude: 0.090530} with n call spatial.addNode('bar', n) yield node return node"); + private DatabaseManagementService databases; + private GraphDatabaseService db; + + @Before + public void setUp() throws KernelException, IOException { + Path dbRoot = new File("target/procedures").toPath(); + FileUtils.deleteDirectory(dbRoot); + databases = new TestDatabaseManagementServiceBuilder(dbRoot) + .setConfig(GraphDatabaseSettings.procedure_unrestricted, List.of("spatial.*")) + .setConfig(GraphDatabaseInternalSettings.trace_cursors, true) + .impermanent().build(); + db = databases.database(DEFAULT_DATABASE_NAME); + registerProceduresAndFunctions(db, SpatialProcedures.class); + } + + @After + public void tearDown() { + databases.shutdown(); + } + + public static void testCall(GraphDatabaseService db, String call, Consumer> consumer) { + testCall(db, call, null, consumer); + } + + public static Map map(Object... values) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i < values.length; i += 2) { + map.put(values[i].toString(), values[i + 1]); + } + return map; + } + + public static void testCall(GraphDatabaseService db, String call, Map params, + Consumer> consumer) { + testCall(db, call, params, consumer, true); + } + + public static void testCallFails(GraphDatabaseService db, String call, Map params, String error) { + try { + testResult(db, call, params, (res) -> { + while (res.hasNext()) { + res.next(); + } + }); + fail("Expected an exception containing '" + error + "', but no exception was thrown"); + } catch (Exception e) { + assertTrue(e.getMessage().contains(error)); + } + } + + public static void testCall(GraphDatabaseService db, String call, Map params, + Consumer> consumer, boolean onlyOne) { + testResult(db, call, params, (res) -> { + Assert.assertTrue("Expect at least one result but got none: " + call, res.hasNext()); + Map row = res.next(); + consumer.accept(row); + if (onlyOne) { + Assert.assertFalse("Expected only one result, but there are more", res.hasNext()); + } + }); + } + + public static void testCallCount(GraphDatabaseService db, String call, Map params, int count) { + testResult(db, call, params, (res) -> { + int numLeft = count; + while (numLeft > 0) { + assertTrue("Expected " + count + " results but found only " + (count - numLeft), res.hasNext()); + res.next(); + numLeft--; + } + Assert.assertFalse("Expected " + count + " results but there are more", res.hasNext()); + }); + } + + public static void testResult(GraphDatabaseService db, String call, Consumer resultConsumer) { + testResult(db, call, null, resultConsumer); + } + + public static void testResult(GraphDatabaseService db, String call, Map params, + Consumer resultConsumer) { + try (Transaction tx = db.beginTx()) { + Map p = (params == null) ? map() : params; + resultConsumer.accept(tx.execute(call, p)); + tx.commit(); + } + } + + public static void registerProceduresAndFunctions(GraphDatabaseService db, Class procedure) + throws KernelException { + GlobalProcedures procedures = ((GraphDatabaseAPI) db).getDependencyResolver() + .resolveDependency(GlobalProcedures.class); + procedures.registerProcedure(procedure); + procedures.registerFunction(procedure); + } + + private Layer makeLayerOfVariousTypes(SpatialDatabaseService spatial, Transaction tx, String name, int index) { + switch (index % 3) { + case 0: + return spatial.getOrCreateSimplePointLayer(tx, name, SpatialDatabaseService.RTREE_INDEX_NAME, "x", "y"); + case 1: + return spatial.getOrCreateNativePointLayer(tx, name, SpatialDatabaseService.RTREE_INDEX_NAME, + "location"); + default: + return spatial.getOrCreateDefaultLayer(tx, name); + } + } + + private void makeOldSpatialModel(Transaction tx, String... layers) { + KernelTransaction ktx = ((InternalTransaction) tx).kernelTransaction(); + SpatialDatabaseService spatial = new SpatialDatabaseService( + new IndexManager((GraphDatabaseAPI) db, ktx.securityContext())); + ArrayList layerNodes = new ArrayList<>(); + int index = 0; + // First create a set of layers + for (String name : layers) { + Layer layer = makeLayerOfVariousTypes(spatial, tx, name, index); + layerNodes.add(layer.getLayerNode(tx)); + index++; + } + // Then downgrade to old format, without label and with reference node and relationships + Node root = ReferenceNodes.createDeprecatedReferenceNode(tx, "spatial_root"); + for (Node node : layerNodes) { + node.removeLabel(LABEL_LAYER); + root.createRelationshipTo(node, SpatialRelationshipTypes.LAYER); + } + } + + @Test + public void old_spatial_model_throws_errors() { + try (Transaction tx = db.beginTx()) { + makeOldSpatialModel(tx, "layer1", "layer2", "layer3"); + tx.commit(); + } + testCallFails(db, "CALL spatial.layers", null, + "Old reference node exists - please upgrade the spatial database to the new format"); + } + + @Test + public void old_spatial_model_can_be_upgraded() { + try (Transaction tx = db.beginTx()) { + makeOldSpatialModel(tx, "layer1", "layer2", "layer3"); + tx.commit(); + } + testCallFails(db, "CALL spatial.layers", null, + "Old reference node exists - please upgrade the spatial database to the new format"); + testCallCount(db, "CALL spatial.upgrade", null, 3); + testCallCount(db, "CALL spatial.layers", null, 3); + } + + @Test + public void add_node_to_non_existing_layer() { + execute("CALL spatial.addPointLayer('some_name')"); + Node node = createNode("CREATE (n:Point {latitude:60.1,longitude:15.2}) RETURN n", "n"); + testCallFails(db, "CALL spatial.addNode.byId('wrong_name',$nodeId)", map("nodeId", node.getElementId()), + "No such layer 'wrong_name'"); + } + + @Test + public void add_node_point_layer() { + execute("CALL spatial.addPointLayer('points')"); + executeWrite("CREATE (n:Point {latitude:60.1,longitude:15.2})"); + Node node = createNode("MATCH (n:Point) WITH n CALL spatial.addNode('points',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('points',{longitude:15.0,latitude:60.0},{longitude:15.3, latitude:60.2})", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('points',{longitude:15.0,latitude:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void add_node_and_search_bbox_and_distance() { + execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); + Node node = createNode( + "CREATE (n:Node {lat:60.1,lon:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + // This tests issue https://github.com/neo4j-contrib/spatial/issues/298 + public void add_node_point_layer_and_search_multiple_points_precision() { + execute("CALL spatial.addPointLayer('bar')"); + execute("create (n:Point) set n={latitude: 52.2029252, longitude: 0.0905302} with n call spatial.addNode('bar', n) yield node return node"); + execute("create (n:Point) set n={latitude: 52.202925, longitude: 0.090530} with n call spatial.addNode('bar', n) yield node return node"); // long countLow = execute("call spatial.withinDistance('bar', {latitude:52.202925,longitude:0.0905302}, 100) YIELD node RETURN node"); // assertThat("Expected two nodes when using low precision", countLow, equalTo(2L)); - long countHigh = execute("call spatial.withinDistance('bar', {latitude:52.2029252,longitude:0.0905302}, 100) YIELD node RETURN node"); - MatcherAssert.assertThat("Expected two nodes when using high precision", countHigh, equalTo(2L)); - } - - @Test - public void add_node_and_search_bbox_and_distance_geohash() { - execute("CALL spatial.addPointLayerGeohash('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void add_node_and_search_bbox_and_distance_zorder() { - execute("CALL spatial.addPointLayerZOrder('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void add_node_and_search_bbox_and_distance_hilbert() { - execute("CALL spatial.addPointLayerHilbert('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - // This tests issue https://github.com/neo4j-contrib/spatial/issues/298 - public void add_node_point_layer_and_search_multiple_points_precision_geohash() { - execute("CALL spatial.addPointLayerGeohash('bar')"); - execute("create (n:Point) set n={latitude: 52.2029252, longitude: 0.0905302} with n call spatial.addNode('bar', n) yield node return node"); - execute("create (n:Point) set n={latitude: 52.202925, longitude: 0.090530} with n call spatial.addNode('bar', n) yield node return node"); + long countHigh = execute( + "call spatial.withinDistance('bar', {latitude:52.2029252,longitude:0.0905302}, 100) YIELD node RETURN node"); + MatcherAssert.assertThat("Expected two nodes when using high precision", countHigh, equalTo(2L)); + } + + @Test + public void add_node_and_search_bbox_and_distance_geohash() { + execute("CALL spatial.addPointLayerGeohash('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void add_node_and_search_bbox_and_distance_zorder() { + execute("CALL spatial.addPointLayerZOrder('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void add_node_and_search_bbox_and_distance_hilbert() { + execute("CALL spatial.addPointLayerHilbert('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0},{lon:15.3, lat:60.2})", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + // This tests issue https://github.com/neo4j-contrib/spatial/issues/298 + public void add_node_point_layer_and_search_multiple_points_precision_geohash() { + execute("CALL spatial.addPointLayerGeohash('bar')"); + execute("create (n:Point) set n={latitude: 52.2029252, longitude: 0.0905302} with n call spatial.addNode('bar', n) yield node return node"); + execute("create (n:Point) set n={latitude: 52.202925, longitude: 0.090530} with n call spatial.addNode('bar', n) yield node return node"); // long countLow = execute("call spatial.withinDistance('bar', {latitude:52.202925,longitude:0.0905302}, 100) YIELD node RETURN node"); // assertEquals("Expected two nodes when using low precision", countLow, equalTo(2L)); - long countHigh = execute("call spatial.withinDistance('bar', {latitude:52.2029252,longitude:0.0905302}, 100) YIELD node RETURN node"); - assertEquals("Expected two nodes when using high precision",2L, countHigh); - } - - // - // Testing interaction between Neo4j Spatial and the Neo4j 3.0 Point type (point() and distance() functions) - // - - @Test - public void create_point_and_distance() { - double distance = (Double) executeObject("WITH point({latitude: 5.0, longitude: 4.0}) as geometry RETURN point.distance(geometry, point({latitude: 5.0, longitude: 4.0})) as distance", "distance"); - System.out.println(distance); - } - - @Test - // TODO: Support this once procedures are able to return Geometry types - public void create_point_geometry_and_distance() { - double distance = (double) executeObject("WITH point({latitude: 5.0, longitude: 4.0}) as geom WITH spatial.asGeometry(geom) AS geometry RETURN point.distance(geometry, point({latitude: 5.0, longitude: 4.0})) as distance", "distance"); - System.out.println(distance); - } - - @Test - public void create_point_and_return() { - Object geometry = executeObject("RETURN point({latitude: 5.0, longitude: 4.0}) as geometry", "geometry"); - assertTrue("Should be Geometry type", geometry instanceof Geometry); - } - - @Test - public void create_point_geometry_return() { - Object geometry = executeObject("WITH point({latitude: 5.0, longitude: 4.0}) as geom RETURN spatial.asGeometry(geom) AS geometry", "geometry"); - assertTrue("Should be Geometry type", geometry instanceof Geometry); - } - - @Test - public void literal_geometry_return() { - Object geometry = executeObject("WITH spatial.asGeometry({latitude: 5.0, longitude: 4.0}) AS geometry RETURN geometry", "geometry"); - assertTrue("Should be Geometry type", geometry instanceof Geometry); - } - - @Test - public void create_node_decode_to_geometry() { - execute("CALL spatial.addWKTLayer('geom','geom')"); - Object geometry = executeObject("CREATE (n:Node {geom:'POINT(4.0 5.0)'}) RETURN spatial.decodeGeometry('geom',n) AS geometry", "geometry"); - assertTrue("Should be Geometry type", geometry instanceof Geometry); - } - - @Test - // TODO: Currently this only works for point geometries because Neo4k 3.4 can only return Point geometries from procedures - public void create_node_and_convert_to_geometry() { - execute("CALL spatial.addWKTLayer('geom','geom')"); - Geometry geom = (Geometry) executeObject("CREATE (n:Node {geom:'POINT(4.0 5.0)'}) RETURN spatial.decodeGeometry('geom',n) AS geometry", "geometry"); - double distance = (Double) executeObject("RETURN point.distance($geom, point({y: 6.0, x: 4.0})) as distance", map("geom", geom), "distance"); - MatcherAssert.assertThat("Expected the cartesian distance of 1.0", distance, closeTo(1.0, 0.00001)); - } - - @Test - // TODO: Currently this only works for point geometries because Neo4j 3.4 can only return Point geometries from procedures - public void create_point_and_pass_as_param() { - Geometry geom = (Geometry) executeObject("RETURN point({latitude: 5.0, longitude: 4.0}) as geometry", "geometry"); - double distance = (Double) executeObject("WITH spatial.asGeometry($geom) AS geometry RETURN point.distance(geometry, point({latitude: 5.1, longitude: 4.0})) as distance", map("geom", geom), "distance"); - MatcherAssert.assertThat("Expected the geographic distance of 11132km", distance, closeTo(11132.0, 1.0)); - } - - private long execute(String statement) { - try (Transaction tx = db.beginTx()) { - long count = Iterators.count(tx.execute(statement)); - tx.commit(); - return count; - } - } - - private long execute(String statement, Map params) { - try (Transaction tx = db.beginTx()) { - long count = Iterators.count(tx.execute(statement, params)); - tx.commit(); - return count; - } - } - - private void executeWrite(String call) { - try (Transaction tx = db.beginTx()) { - tx.execute(call).accept(v -> true); - tx.commit(); - } - } - - private Node createNode(String call, String column) { - Node node; - try (Transaction tx = db.beginTx()) { - ResourceIterator nodes = tx.execute(call).columnAs(column); - node = (Node) nodes.next(); - nodes.close(); - tx.commit(); - } - return node; - } - - private Object executeObject(String call, String column) { - Object obj; - try (Transaction tx = db.beginTx()) { - ResourceIterator values = tx.execute(call).columnAs(column); - obj = values.next(); - values.close(); - tx.commit(); - } - return obj; - } - - private Object executeObject(String call, Map params, String column) { - Object obj; - try (Transaction tx = db.beginTx()) { - Map p = (params == null) ? map() : params; - ResourceIterator values = tx.execute(call, p).columnAs(column); - obj = values.next(); - values.close(); - tx.commit(); - } - return obj; - } - - @Test - public void create_a_pointlayer_with_x_and_y() { - testCall(db, "CALL spatial.addPointLayerXY('geom','lon','lat')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_a_pointlayer_with_config() { - testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_a_pointlayer_with_config_on_existing_wkt_layer() { - execute("CALL spatial.addWKTLayer('geom','wkt')"); - try { - testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - fail("Expected exception to be thrown"); - } catch (Exception e) { - assertTrue(e.getMessage().contains("Cannot create existing layer")); - } - } - - @Test - public void create_a_pointlayer_with_config_on_existing_osm_layer() { - execute("CALL spatial.addLayer('geom','OSM','')"); - try { - testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - fail("Expected exception to be thrown"); - } catch (Exception e) { - assertTrue(e.getMessage().contains("Cannot create existing layer")); - } - } - - @Test - public void create_a_pointlayer_with_rtree() { - testCall(db, "CALL spatial.addPointLayer('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_a_pointlayer_with_geohash() { - testCall(db, "CALL spatial.addPointLayerGeohash('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_a_pointlayer_with_zorder() { - testCall(db, "CALL spatial.addPointLayerZOrder('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_a_pointlayer_with_hilbert() { - testCall(db, "CALL spatial.addPointLayerHilbert('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void create_and_delete_a_pointlayer_with_rtree() { - testCall(db, "CALL spatial.addPointLayer('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - testCallCount(db, "CALL spatial.layers()", null, 1); - execute("CALL spatial.removeLayer('geom')"); - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void create_and_delete_a_pointlayer_with_geohash() { - testCall(db, "CALL spatial.addPointLayerGeohash('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - testCallCount(db, "CALL spatial.layers()", null, 1); - execute("CALL spatial.removeLayer('geom')"); - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void create_and_delete_a_pointlayer_with_zorder() { - testCall(db, "CALL spatial.addPointLayerZOrder('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - testCallCount(db, "CALL spatial.layers()", null, 1); - execute("CALL spatial.removeLayer('geom')"); - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void create_and_delete_a_pointlayer_with_hilbert() { - testCall(db, "CALL spatial.addPointLayerHilbert('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - testCallCount(db, "CALL spatial.layers()", null, 1); - execute("CALL spatial.removeLayer('geom')"); - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void create_a_simple_pointlayer_using_named_encoder() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','SimplePointEncoder','')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty("layer")); - assertEquals("org.neo4j.gis.spatial.encoders.SimplePointEncoder", node.getProperty("geomencoder")); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty("layer_class")); - assertFalse(node.hasProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_simple_pointlayer_using_named_and_configured_encoder() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','SimplePointEncoder','x:y:mbr')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty(PROP_LAYER)); - assertEquals("org.neo4j.gis.spatial.encoders.SimplePointEncoder", node.getProperty(PROP_GEOMENCODER)); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); - assertEquals("x:y:mbr", node.getProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_native_pointlayer_using_named_encoder() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty(PROP_LAYER)); - assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); - assertFalse(node.hasProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_native_pointlayer_using_named_and_configured_encoder() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty(PROP_LAYER)); - assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); - assertEquals("pos:mbr", node.getProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_native_pointlayer_using_named_and_configured_encoder_with_cartesian() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr:Cartesian')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty(PROP_LAYER)); - assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); - assertEquals("pos:mbr:Cartesian", node.getProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_native_pointlayer_using_named_and_configured_encoder_with_geographic() { - testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr:WGS-84')", (r) -> { - Node node = dump((Node) r.get("node")); - assertEquals("geom", node.getProperty(PROP_LAYER)); - assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); - assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); - assertEquals("pos:mbr:WGS-84", node.getProperty(PROP_GEOMENCODER_CONFIG)); - }); - } - - @Test - public void create_a_wkt_layer_using_know_format() { - testCall(db, "CALL spatial.addLayer('geom','WKT',null)", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - } - - @Test - public void list_layer_names() { - String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; - execute("CALL spatial.addWKTLayer('geom','wkt')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", wkt)); - - testCall(db, "CALL spatial.layers()", (r) -> { - assertEquals("geom", r.get("name")); - assertEquals("EditableLayer(name='geom', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", r.get("signature")); - }); - } - - @Test - public void add_and_remove_layer() { - execute("CALL spatial.addWKTLayer('geom','wkt')"); - testCallCount(db, "CALL spatial.layers()", null, 1); - execute("CALL spatial.removeLayer('geom')"); - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void add_and_remove_multiple_layers() { - int NUM_LAYERS = 100; - String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; - for (int i = 0; i < NUM_LAYERS; i++) { - String name = "wktLayer_" + i; - testCallCount(db, "CALL spatial.layers()", null, i); - execute("CALL spatial.addWKTLayer($layerName,'wkt')", map("layerName", name)); - execute("CALL spatial.addWKT($layerName,$wkt)", map("wkt", wkt, "layerName", name)); - testCallCount(db, "CALL spatial.layers()", null, i + 1); - } - for (int i = 0; i < NUM_LAYERS; i++) { - String name = "wktLayer_" + i; - testCallCount(db, "CALL spatial.layers()", null, NUM_LAYERS - i); - execute("CALL spatial.removeLayer($layerName)", map("layerName", name)); - testCallCount(db, "CALL spatial.layers()", null, NUM_LAYERS - i - 1); - } - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - @Test - public void get_and_set_feature_attributes() { - execute("CALL spatial.addWKTLayer('geom','wkt')"); - testCallCount(db, "CALL spatial.layers()", null, 1); - testCallCount(db, "CALL spatial.getFeatureAttributes('geom')", null, 0); - execute("CALL spatial.setFeatureAttributes('geom',['name','type','color'])"); - testCallCount(db, "CALL spatial.getFeatureAttributes('geom')", null, 3); - } - - @Test - public void list_spatial_procedures() { - testResult(db, "CALL spatial.procedures()", (res) -> { - Map procs = new LinkedHashMap<>(); - while (res.hasNext()) { - Map r = res.next(); - procs.put(r.get("name").toString(), r.get("signature").toString()); - } - for (String key : procs.keySet()) { - System.out.println(key + ": " + procs.get(key)); - } - assertEquals("spatial.procedures() :: (name :: STRING, signature :: STRING)", procs.get("spatial.procedures")); - assertEquals("spatial.layers() :: (name :: STRING, signature :: STRING)", procs.get("spatial.layers")); - assertEquals("spatial.layer(name :: STRING) :: (node :: NODE)", procs.get("spatial.layer")); - assertEquals("spatial.addLayer(name :: STRING, type :: STRING, encoderConfig :: STRING) :: (node :: NODE)", procs.get("spatial.addLayer")); - assertEquals("spatial.addNode(layerName :: STRING, node :: NODE) :: (node :: NODE)", procs.get("spatial.addNode")); - assertEquals("spatial.addWKT(layerName :: STRING, geometry :: STRING) :: (node :: NODE)", procs.get("spatial.addWKT")); - assertEquals("spatial.intersects(layerName :: STRING, geometry :: ANY) :: (node :: NODE)", procs.get("spatial.intersects")); - }); - } - - @Test - public void list_layer_types() { - testResult(db, "CALL spatial.layerTypes()", (res) -> { - Map procs = new LinkedHashMap<>(); - while (res.hasNext()) { - Map r = res.next(); - procs.put(r.get("name").toString(), r.get("signature").toString()); - } - for (String key : procs.keySet()) { - System.out.println(key + ": " + procs.get(key)); - } - assertEquals("RegisteredLayerType(name='SimplePoint', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", procs.get("simplepoint")); - assertEquals("RegisteredLayerType(name='NativePoint', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='location')", procs.get("nativepoint")); - assertEquals("RegisteredLayerType(name='WKT', geometryEncoder=WKTGeometryEncoder, layerClass=EditableLayerImpl, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='geometry')", procs.get("wkt")); - assertEquals("RegisteredLayerType(name='WKB', geometryEncoder=WKBGeometryEncoder, layerClass=EditableLayerImpl, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='geometry')", procs.get("wkb")); - assertEquals("RegisteredLayerType(name='Geohash', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerGeohashPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", procs.get("geohash")); - assertEquals("RegisteredLayerType(name='ZOrder', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerZOrderPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", procs.get("zorder")); - assertEquals("RegisteredLayerType(name='Hilbert', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerHilbertPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", procs.get("hilbert")); - assertEquals("RegisteredLayerType(name='NativeGeohash', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerGeohashPointIndex, crs='WGS84(DD)', defaultConfig='location')", procs.get("nativegeohash")); - assertEquals("RegisteredLayerType(name='NativeZOrder', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerZOrderPointIndex, crs='WGS84(DD)', defaultConfig='location')", procs.get("nativezorder")); - assertEquals("RegisteredLayerType(name='NativeHilbert', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerHilbertPointIndex, crs='WGS84(DD)', defaultConfig='location')", procs.get("nativehilbert")); - }); - } - - @Test - public void find_layer() { - String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; - execute("CALL spatial.addWKTLayer('geom','wkt')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", wkt)); - - testCall(db, "CALL spatial.layer('geom')", (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); - testCallFails(db, "CALL spatial.layer('badname')", null, "No such layer 'badname'"); - } - - @Test - public void add_a_node_to_the_spatial_rtree_index_for_simple_points() { - execute("CALL spatial.addPointLayer('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_the_spatial_geohash_index_for_simple_points() { - execute("CALL spatial.addPointLayerGeohash('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_the_spatial_zorder_index_for_simple_points() { - execute("CALL spatial.addPointLayerZOrder('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_the_spatial_hilbert_index_for_simple_points() { - execute("CALL spatial.addPointLayerHilbert('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_multiple_different_indexes_for_both_simple_and_native_points() { - String[] encoders = new String[]{"Simple", "Native"}; - String[] indexes = new String[]{"Geohash", "ZOrder", "Hilbert", "RTree"}; - for (String encoder : encoders) { - String procName = (encoder.equalsIgnoreCase("Native")) ? "addNativePointLayer" : "addPointLayer"; - for (String indexType : indexes) { - String layerName = (encoder + indexType).toLowerCase(); - String query = "CALL spatial." + procName + (indexType.equals("RTree") ? "" : indexType) + "('" + layerName + "')"; - execute(query); - } - } - testResult(db, "CALL spatial.layers()", (res) -> { - while (res.hasNext()) { - Map r = res.next(); - String encoder = r.get("name").toString().contains("native") ? "NativePointEncoder" : "SimplePointEncoder"; - MatcherAssert.assertThat("Expect simple:native encoders to appear in simple:native layers", r.get("signature").toString(), containsString(encoder)); - } - }); - testCallCount(db, "CALL spatial.layers()", null, indexes.length * encoders.length); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) SET n.location=point(n) RETURN n", "n"); - for (String encoder : encoders) { - for (String indexType : indexes) { - String layerName = (encoder + indexType).toLowerCase(); - testCall(db, "MATCH (node:Node) RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('" + layerName + "',n) YIELD node RETURN node", r -> assertEquals(node, r.get("node"))); - testCall(db, "CALL spatial.withinDistance('" + layerName + "',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - } - for (String encoder : encoders) { - for (String indexType : indexes) { - String layerName = (encoder + indexType).toLowerCase(); - execute("CALL spatial.removeLayer('" + layerName + "')"); - } - } - testCallCount(db, "CALL spatial.layers()", null, 0); - } - - - @Test - public void testDistanceNode() { - execute("CALL spatial.addPointLayer('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void testDistanceNodeWithGeohashIndex() { - execute("CALL spatial.addPointLayer('geom','geohash')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void testDistanceNodeGeohash() { - execute("CALL spatial.addPointLayerGeohash('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void testDistanceNodeZOrder() { - execute("CALL spatial.addPointLayerZOrder('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void testDistanceNodeHilbert() { - execute("CALL spatial.addPointLayerHilbert('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_the_spatial_index_short() { - execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); - Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_a_node_to_the_spatial_index_short_with_geohash() { - execute("CALL spatial.addPointLayerXY('geom','lon','lat','geohash')"); - Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) RETURN n", "n"); - testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", r -> Assert.assertEquals(node, r.get("node"))); - } - - @Test - public void add_two_nodes_to_the_spatial_layer() { - execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); - String node1; - String node2; - try (Transaction tx = db.beginTx()) { - Result result = tx.execute("CREATE (n1:Node {lat:60.1,lon:15.2}),(n2:Node {lat:60.1,lon:15.3}) WITH n1,n2 CALL spatial.addNodes('geom',[n1,n2]) YIELD count RETURN n1,n2,count"); - Map row = result.next(); - node1 = ((Node) row.get("n1")).getElementId(); - node2 = ((Node) row.get("n2")).getElementId(); - long count = (Long) row.get("count"); - Assert.assertEquals(2L, count); - result.close(); - tx.commit(); - } - testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", res -> { - assertTrue(res.hasNext()); - assertEquals(node1, ((Node) res.next().get("node")).getElementId()); - assertTrue(res.hasNext()); - assertEquals(node2, ((Node) res.next().get("node")).getElementId()); - assertFalse(res.hasNext()); - }); - try (Transaction tx = db.beginTx()) { - Node node = (Node) tx.execute("MATCH (node) WHERE elementId(node) = $nodeId RETURN node", map("nodeId", node1)).next().get("node"); - Result removeResult = tx.execute("CALL spatial.removeNode('geom',$node) YIELD nodeId RETURN nodeId", map("node", node)); - Assert.assertEquals(node1, removeResult.next().get("nodeId")); - removeResult.close(); - tx.commit(); - } - testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", res -> { - assertTrue(res.hasNext()); - assertEquals(node2, ((Node) res.next().get("node")).getElementId()); - assertFalse(res.hasNext()); - }); - try (Transaction tx = db.beginTx()) { - Result removeResult = tx.execute("CALL spatial.removeNode.byId('geom',$nodeId) YIELD nodeId RETURN nodeId", map("nodeId", node2)); - Assert.assertEquals(node2, removeResult.next().get("nodeId")); - removeResult.close(); - tx.commit(); - } - testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", res -> assertFalse(res.hasNext())); - } - - @Test - public void add_many_nodes_to_the_simple_point_layer_using_addNodes() { - // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison - int count = 1000; - execute("CALL spatial.addLayer('simple_poi','SimplePoint','')"); - String query = "UNWIND range(1,$count) as i\n" + - "CREATE (n:Point {id:i, latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)})\n" + - "WITH collect(n) as points\n" + - "CALL spatial.addNodes('simple_poi',points) YIELD count\n" + - "RETURN count"; - testCountQuery("addNodes", query, count, "count", map("count", count)); - testRemoveNodes("simple_poi", count); - } - - @Test - public void add_many_nodes_to_the_simple_point_layer_using_addNode() { - // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison - int count = 1000; - execute("CALL spatial.addLayer('simple_poi','SimplePoint','')"); - String query = "UNWIND range(1,$count) as i\n" + - "CREATE (n:Point {id:i, latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)})\n" + - "WITH n\n" + - "CALL spatial.addNode('simple_poi',n) YIELD node\n" + - "RETURN count(node)"; - testCountQuery("addNode", query, count, "count(node)", map("count", count)); - testRemoveNode("simple_poi", count); - } - - @Test - public void add_many_nodes_to_the_native_point_layer_using_addNodes() { - // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison - int count = 1000; - execute("CALL spatial.addLayer('native_poi','NativePoint','')"); - String query = "UNWIND range(1,$count) as i\n" + - "WITH i, Point({latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)}) AS location\n" + - "CREATE (n:Point {id: i, location:location})\n" + - "WITH collect(n) as points\n" + - "CALL spatial.addNodes('native_poi',points) YIELD count\n" + - "RETURN count"; - testCountQuery("addNodes", query, count, "count", map("count", count)); - testRemoveNodes("native_poi", count); - } - - @Test - public void add_many_nodes_to_the_native_point_layer_using_addNode() { - // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison - int count = 1000; - execute("CALL spatial.addLayer('native_poi','NativePoint','')"); - String query = "UNWIND range(1,$count) as i\n" + - "WITH i, Point({latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)}) AS location\n" + - "CREATE (n:Point {id: i, location:location})\n" + - "WITH n\n" + - "CALL spatial.addNode('native_poi',n) YIELD node\n" + - "RETURN count(node)"; - testCountQuery("addNode", query, count, "count(node)", map("count", count)); - testRemoveNode("native_poi", count); - } - - private void testRemoveNode(String layer, int count) { - // Check all nodes are there - testCountQuery("withinDistance", "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", count, "count(node)", null); - // Now remove half the points - String remove = "UNWIND range(1,$count) as i\n" + - "MATCH (n:Point {id:i})\n" + - "WITH n\n" + - "CALL spatial.removeNode('" + layer + "',n) YIELD nodeId\n" + - "RETURN count(nodeId)"; - testCountQuery("removeNode", remove, count / 2, "count(nodeId)", map("count", count / 2)); - // Check that only half remain - testCountQuery("withinDistance", "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", count / 2, "count(node)", null); - } - - private void testRemoveNodes(String layer, int count) { - // Check all nodes are there - testCountQuery("withinDistance", "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", count, "count(node)", null); - // Now remove half the points - String remove = "UNWIND range(1,$count) as i\n" + - "MATCH (n:Point {id:i})\n" + - "WITH collect(n) as points\n" + - "CALL spatial.removeNodes('" + layer + "',points) YIELD count\n" + - "RETURN count"; - testCountQuery("removeNodes", remove, count / 2, "count", map("count", count / 2)); - // Check that only half remain - testCountQuery("withinDistance", "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", count / 2, "count(node)", null); - } - - @Test - public void import_shapefile() { - testCountQuery("importShapefile", "CALL spatial.importShapefile('shp/highway.shp')", 143, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_shapefile_without_extension() { - testCountQuery("importShapefile", "CALL spatial.importShapefile('shp/highway')", 143, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_shapefile_to_layer() { - execute("CALL spatial.addWKTLayer('geom','wkt')"); - testCountQuery("importShapefileToLayer", "CALL spatial.importShapefileToLayer('geom','shp/highway.shp')", 143, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm() { - testCountQuery("importOSM", "CALL spatial.importOSM('map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_twice_should_fail() { - testCountQuery("importOSM", "CALL spatial.importOSM('map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - testCallFails(db, "CALL spatial.importOSM('map.osm')", null, "Layer already exists: 'map.osm'"); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_without_extension() { - testCountQuery("importOSM", "CALL spatial.importOSM('map')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_to_layer() { - execute("CALL spatial.addLayer('geom','OSM','')"); - testCountQuery("importOSMToLayer", "CALL spatial.importOSMToLayer('geom','map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_twice_should_pass_with_different_layers() { - execute("CALL spatial.addLayer('geom1','OSM','')"); - execute("CALL spatial.addLayer('geom2','OSM','')"); - - testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom1','map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 2); - testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217); - testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, 0); - - testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom2','map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 2); - testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217); - testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217); - } - - @Ignore - public void import_cracow_to_layer() { - execute("CALL spatial.addLayer('geom','OSM','')"); - testCountQuery("importCracowToLayer", "CALL spatial.importOSMToLayer('geom','issue-347/cra.osm')", 256253, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_to_layer_without_changesets() { - execute("CALL spatial.addLayer('osm_example','OSM','')"); - testCountQuery("importOSMToLayerWithoutChangesets", "CALL spatial.importOSMToLayer('osm_example','sample.osm')", 1, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - } - - @Test - public void import_osm_and_add_geometry() { - execute("CALL spatial.addLayer('geom','OSM','')"); - testCountQuery("importOSMToLayerAndAddGeometry", "CALL spatial.importOSMToLayer('geom','map.osm')", 55, "count", null); - testCallCount(db, "CALL spatial.layers()", null, 1); - testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", null, 0); - testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217); - - // Adding a point to the layer - Node node = createNode("CALL spatial.addWKT('geom', 'POINT(6.3740429666 50.93676351666)') YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", r -> assertEquals(node, r.get("node"))); - testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", null, 1); - testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},10000)", null, 218); - } - - @Test - public void import_osm_and_polygons_withinDistance() { - Map params = map("osmFile", "withinDistance.osm", "busShelterID", 2938842290L); - execute("CALL spatial.addLayer('geom','OSM','')"); - testCountQuery("importOSMAndPolygonsWithinDistance", "CALL spatial.importOSMToLayer('geom',$osmFile)", 74, "count", params); - testCallCount(db, "CALL spatial.layers()", null, 1); - testCallCount(db, "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance RETURN node, distance", params, 516); - testResult(db, "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance WITH node, distance ORDER BY distance LIMIT 20 MATCH (node)<-[:GEOM]-(osmNode) RETURN node, distance, osmNode, properties(osmNode) as props", - params, res -> { - while (res.hasNext()) { - Map r = res.next(); - assertThat("Result should have 'node'", r, hasKey("node")); - assertThat("Result should have 'distance'", r, hasKey("distance")); - assertThat("Result should have 'osmNode'", r, hasKey("osmNode")); - assertThat("Result should have 'props'", r, hasKey("props")); - Node node = (Node) r.get("node"); - double distance = (Double) r.get("distance"); - Node osmNode = (Node) r.get("osmNode"); - @SuppressWarnings("rawtypes") Map props = (Map) r.get("props"); - System.out.println("(node[" + node.getElementId() + "])<-[:GEOM {distance:" + distance + "}]-(osmNode[" + osmNode.getElementId() + "] " + props + ") "); - assertThat("Node should have either way_osm_id or node_osm_id", props, anyOf(hasKey("node_osm_id"), hasKey("way_osm_id"))); - } - }); - testResult(db, "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance WITH node, distance ORDER BY distance LIMIT 20 MATCH (n) WHERE elementId(n)=elementId(node) RETURN node, distance, spatial.decodeGeometry('geom',n) AS geometry", - params, res -> { - while (res.hasNext()) { - Map r = res.next(); - assertThat("Result should have 'node'", r, hasKey("node")); - assertThat("Result should have 'distance'", r, hasKey("distance")); - assertThat("Result should have 'geometry'", r, hasKey("geometry")); - Node node = (Node) r.get("node"); - double distance = (Double) r.get("distance"); - Object geometry = r.get("geometry"); - System.out.println(node.toString() + " at " + distance + ": " + geometry); - if (geometry instanceof Point) { - assertThat("Point has 2D coordinates", ((Point) geometry).getCoordinate().getCoordinate().length, equalTo(2)); - } else if (geometry instanceof Map) { - Map map = (Map) geometry; - assertThat("Geometry should contain a type", map, hasKey("type")); - assertThat("Geometry should contain coordinates", map, hasKey("coordinates")); - assertThat("Geometry should not be a point", map.get("type"), not(equalTo("Point"))); - } else { - fail("Geometry should be either a point or a Map containing coordinates"); - } - } - }); - } - - private void testCountQuery(String name, String query, long count, String column, Map params) { - // warmup - try (Transaction tx = db.beginTx()) { - Result results = tx.execute("EXPLAIN " + query, params == null ? map() : params); - results.close(); - tx.commit(); - } - long start = System.currentTimeMillis(); - testResult(db, query, params, res -> { - assertTrue("Expected a single result", res.hasNext()); - long c = (Long) res.next().get(column); - assertFalse("Expected a single result", res.hasNext()); - Assert.assertEquals("Expected count of " + count + " nodes but got " + c, count, c); - } - ); - System.out.println(name + " query took " + (System.currentTimeMillis() - start) + "ms - " + params); - } - - @Test - public void find_geometries_in_a_bounding_box_short() { - execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); - Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void find_geometries_in_a_bounding_box() { - execute("CALL spatial.addPointLayer('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void find_geometries_in_a_polygon() { - execute("CALL spatial.addPointLayer('geom')"); - executeWrite("UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); - String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; - testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", r -> assertEquals("b", r.get("name"))); - } - - @Test - public void find_geometries_in_a_bounding_box_geohash() { - execute("CALL spatial.addPointLayerGeohash('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void find_geometries_in_a_polygon_geohash() { - execute("CALL spatial.addPointLayerGeohash('geom')"); - executeWrite("UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); - String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; - testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", r -> assertEquals("b", r.get("name"))); - } - - @Test - public void find_geometries_in_a_bounding_box_zorder() { - execute("CALL spatial.addPointLayerZOrder('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void find_geometries_in_a_polygon_zorder() { - execute("CALL spatial.addPointLayerZOrder('geom')"); - executeWrite("UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); - String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; - testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", r -> assertEquals("b", r.get("name"))); - } - - @Test - public void find_geometries_in_a_bounding_box_hilbert() { - execute("CALL spatial.addPointLayerHilbert('geom')"); - Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", "node"); - testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", r -> assertEquals(node, r.get("node"))); - } - - @Test - public void find_geometries_in_a_polygon_hilbert() { - execute("CALL spatial.addPointLayerHilbert('geom')"); - executeWrite("UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); - String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; - testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", r -> assertEquals("b", r.get("name"))); - } - - @Test - public void create_a_WKT_layer() { - testCall(db, "CALL spatial.addWKTLayer('geom','wkt')", r -> assertEquals("wkt", dump(((Node) r.get("node"))).getProperty("geomencoder_config"))); - } - - private static Node dump(Node n) { - System.out.printf("id %s props %s%n", n.getElementId(), n.getAllProperties()); - System.out.flush(); - return n; - } - - @Test - public void add_a_WKT_geometry_to_a_layer() { - String lineString = "LINESTRING (15.2 60.1, 15.3 60.1)"; - - execute("CALL spatial.addWKTLayer('geom','wkt')"); - testCall(db, "CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString), - r -> assertEquals(lineString, dump(((Node) r.get("node"))).getProperty("wkt"))); - } - - @Test - public void find_geometries_close_to_a_point_wkt() { - String lineString = "LINESTRING (15.2 60.1, 15.3 60.1)"; - execute("CALL spatial.addLayer('geom','WKT','wkt')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); - testCall(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", r -> assertEquals(lineString, (dump((Node) r.get("node"))).getProperty("wkt"))); - } - - @Test - public void find_geometries_close_to_a_point_geohash() { - String lineString = "POINT (15.2 60.1)"; - execute("CALL spatial.addLayer('geom','geohash','lon:lat')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); - testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); - } - - @Test - public void find_geometries_close_to_a_point_zorder() { - String lineString = "POINT (15.2 60.1)"; - execute("CALL spatial.addLayer('geom','zorder','lon:lat')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); - testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); - } - - @Test - public void find_geometries_close_to_a_point_hilbert() { - String lineString = "POINT (15.2 60.1)"; - execute("CALL spatial.addLayer('geom','hilbert','lon:lat')"); - execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); - testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); - } - - @Test - public void find_no_geometries_using_closest_on_empty_layer() { - execute("CALL spatial.addLayer('geom','WKT','wkt')"); - testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 0); - } + long countHigh = execute( + "call spatial.withinDistance('bar', {latitude:52.2029252,longitude:0.0905302}, 100) YIELD node RETURN node"); + assertEquals("Expected two nodes when using high precision", 2L, countHigh); + } + + // + // Testing interaction between Neo4j Spatial and the Neo4j 3.0 Point type (point() and distance() functions) + // + + @Test + public void create_point_and_distance() { + double distance = (Double) executeObject( + "WITH point({latitude: 5.0, longitude: 4.0}) as geometry RETURN point.distance(geometry, point({latitude: 5.0, longitude: 4.0})) as distance", + "distance"); + System.out.println(distance); + } + + @Test + // TODO: Support this once procedures are able to return Geometry types + public void create_point_geometry_and_distance() { + double distance = (double) executeObject( + "WITH point({latitude: 5.0, longitude: 4.0}) as geom WITH spatial.asGeometry(geom) AS geometry RETURN point.distance(geometry, point({latitude: 5.0, longitude: 4.0})) as distance", + "distance"); + System.out.println(distance); + } + + @Test + public void create_point_and_return() { + Object geometry = executeObject("RETURN point({latitude: 5.0, longitude: 4.0}) as geometry", "geometry"); + assertTrue("Should be Geometry type", geometry instanceof Geometry); + } + + @Test + public void create_point_geometry_return() { + Object geometry = executeObject( + "WITH point({latitude: 5.0, longitude: 4.0}) as geom RETURN spatial.asGeometry(geom) AS geometry", + "geometry"); + assertTrue("Should be Geometry type", geometry instanceof Geometry); + } + + @Test + public void literal_geometry_return() { + Object geometry = executeObject( + "WITH spatial.asGeometry({latitude: 5.0, longitude: 4.0}) AS geometry RETURN geometry", "geometry"); + assertTrue("Should be Geometry type", geometry instanceof Geometry); + } + + @Test + public void create_node_decode_to_geometry() { + execute("CALL spatial.addWKTLayer('geom','geom')"); + Object geometry = executeObject( + "CREATE (n:Node {geom:'POINT(4.0 5.0)'}) RETURN spatial.decodeGeometry('geom',n) AS geometry", + "geometry"); + assertTrue("Should be Geometry type", geometry instanceof Geometry); + } + + @Test + // TODO: Currently this only works for point geometries because Neo4k 3.4 can only return Point geometries from procedures + public void create_node_and_convert_to_geometry() { + execute("CALL spatial.addWKTLayer('geom','geom')"); + Geometry geom = (Geometry) executeObject( + "CREATE (n:Node {geom:'POINT(4.0 5.0)'}) RETURN spatial.decodeGeometry('geom',n) AS geometry", + "geometry"); + double distance = (Double) executeObject("RETURN point.distance($geom, point({y: 6.0, x: 4.0})) as distance", + map("geom", geom), "distance"); + MatcherAssert.assertThat("Expected the cartesian distance of 1.0", distance, closeTo(1.0, 0.00001)); + } + + @Test + // TODO: Currently this only works for point geometries because Neo4j 3.4 can only return Point geometries from procedures + public void create_point_and_pass_as_param() { + Geometry geom = (Geometry) executeObject("RETURN point({latitude: 5.0, longitude: 4.0}) as geometry", + "geometry"); + double distance = (Double) executeObject( + "WITH spatial.asGeometry($geom) AS geometry RETURN point.distance(geometry, point({latitude: 5.1, longitude: 4.0})) as distance", + map("geom", geom), "distance"); + MatcherAssert.assertThat("Expected the geographic distance of 11132km", distance, closeTo(11132.0, 1.0)); + } + + private long execute(String statement) { + try (Transaction tx = db.beginTx()) { + long count = Iterators.count(tx.execute(statement)); + tx.commit(); + return count; + } + } + + private long execute(String statement, Map params) { + try (Transaction tx = db.beginTx()) { + long count = Iterators.count(tx.execute(statement, params)); + tx.commit(); + return count; + } + } + + private void executeWrite(String call) { + try (Transaction tx = db.beginTx()) { + tx.execute(call).accept(v -> true); + tx.commit(); + } + } + + private Node createNode(String call, String column) { + Node node; + try (Transaction tx = db.beginTx()) { + ResourceIterator nodes = tx.execute(call).columnAs(column); + node = (Node) nodes.next(); + nodes.close(); + tx.commit(); + } + return node; + } + + private Object executeObject(String call, String column) { + Object obj; + try (Transaction tx = db.beginTx()) { + ResourceIterator values = tx.execute(call).columnAs(column); + obj = values.next(); + values.close(); + tx.commit(); + } + return obj; + } + + private Object executeObject(String call, Map params, String column) { + Object obj; + try (Transaction tx = db.beginTx()) { + Map p = (params == null) ? map() : params; + ResourceIterator values = tx.execute(call, p).columnAs(column); + obj = values.next(); + values.close(); + tx.commit(); + } + return obj; + } + + @Test + public void create_a_pointlayer_with_x_and_y() { + testCall(db, "CALL spatial.addPointLayerXY('geom','lon','lat')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_a_pointlayer_with_config() { + testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_a_pointlayer_with_config_on_existing_wkt_layer() { + execute("CALL spatial.addWKTLayer('geom','wkt')"); + try { + testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertTrue(e.getMessage().contains("Cannot create existing layer")); + } + } + + @Test + public void create_a_pointlayer_with_config_on_existing_osm_layer() { + execute("CALL spatial.addLayer('geom','OSM','')"); + try { + testCall(db, "CALL spatial.addPointLayerWithConfig('geom','lon:lat')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + fail("Expected exception to be thrown"); + } catch (Exception e) { + assertTrue(e.getMessage().contains("Cannot create existing layer")); + } + } + + @Test + public void create_a_pointlayer_with_rtree() { + testCall(db, "CALL spatial.addPointLayer('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_a_pointlayer_with_geohash() { + testCall(db, "CALL spatial.addPointLayerGeohash('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_a_pointlayer_with_zorder() { + testCall(db, "CALL spatial.addPointLayerZOrder('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_a_pointlayer_with_hilbert() { + testCall(db, "CALL spatial.addPointLayerHilbert('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void create_and_delete_a_pointlayer_with_rtree() { + testCall(db, "CALL spatial.addPointLayer('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + testCallCount(db, "CALL spatial.layers()", null, 1); + execute("CALL spatial.removeLayer('geom')"); + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void create_and_delete_a_pointlayer_with_geohash() { + testCall(db, "CALL spatial.addPointLayerGeohash('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + testCallCount(db, "CALL spatial.layers()", null, 1); + execute("CALL spatial.removeLayer('geom')"); + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void create_and_delete_a_pointlayer_with_zorder() { + testCall(db, "CALL spatial.addPointLayerZOrder('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + testCallCount(db, "CALL spatial.layers()", null, 1); + execute("CALL spatial.removeLayer('geom')"); + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void create_and_delete_a_pointlayer_with_hilbert() { + testCall(db, "CALL spatial.addPointLayerHilbert('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + testCallCount(db, "CALL spatial.layers()", null, 1); + execute("CALL spatial.removeLayer('geom')"); + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void create_a_simple_pointlayer_using_named_encoder() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','SimplePointEncoder','')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty("layer")); + assertEquals("org.neo4j.gis.spatial.encoders.SimplePointEncoder", node.getProperty("geomencoder")); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty("layer_class")); + assertFalse(node.hasProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_simple_pointlayer_using_named_and_configured_encoder() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','SimplePointEncoder','x:y:mbr')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty(PROP_LAYER)); + assertEquals("org.neo4j.gis.spatial.encoders.SimplePointEncoder", node.getProperty(PROP_GEOMENCODER)); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); + assertEquals("x:y:mbr", node.getProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_native_pointlayer_using_named_encoder() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty(PROP_LAYER)); + assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); + assertFalse(node.hasProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_native_pointlayer_using_named_and_configured_encoder() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty(PROP_LAYER)); + assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); + assertEquals("pos:mbr", node.getProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_native_pointlayer_using_named_and_configured_encoder_with_cartesian() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr:Cartesian')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty(PROP_LAYER)); + assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); + assertEquals("pos:mbr:Cartesian", node.getProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_native_pointlayer_using_named_and_configured_encoder_with_geographic() { + testCall(db, "CALL spatial.addLayerWithEncoder('geom','NativePointEncoder','pos:mbr:WGS-84')", (r) -> { + Node node = dump((Node) r.get("node")); + assertEquals("geom", node.getProperty(PROP_LAYER)); + assertEquals("org.neo4j.gis.spatial.encoders.NativePointEncoder", node.getProperty(PROP_GEOMENCODER)); + assertEquals("org.neo4j.gis.spatial.SimplePointLayer", node.getProperty(PROP_LAYER_CLASS)); + assertEquals("pos:mbr:WGS-84", node.getProperty(PROP_GEOMENCODER_CONFIG)); + }); + } + + @Test + public void create_a_wkt_layer_using_know_format() { + testCall(db, "CALL spatial.addLayer('geom','WKT',null)", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + } + + @Test + public void list_layer_names() { + String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; + execute("CALL spatial.addWKTLayer('geom','wkt')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", wkt)); + + testCall(db, "CALL spatial.layers()", (r) -> { + assertEquals("geom", r.get("name")); + assertEquals("EditableLayer(name='geom', encoder=WKTGeometryEncoder(geom='wkt', bbox='bbox'))", + r.get("signature")); + }); + } + + @Test + public void add_and_remove_layer() { + execute("CALL spatial.addWKTLayer('geom','wkt')"); + testCallCount(db, "CALL spatial.layers()", null, 1); + execute("CALL spatial.removeLayer('geom')"); + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void add_and_remove_multiple_layers() { + int NUM_LAYERS = 100; + String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; + for (int i = 0; i < NUM_LAYERS; i++) { + String name = "wktLayer_" + i; + testCallCount(db, "CALL spatial.layers()", null, i); + execute("CALL spatial.addWKTLayer($layerName,'wkt')", map("layerName", name)); + execute("CALL spatial.addWKT($layerName,$wkt)", map("wkt", wkt, "layerName", name)); + testCallCount(db, "CALL spatial.layers()", null, i + 1); + } + for (int i = 0; i < NUM_LAYERS; i++) { + String name = "wktLayer_" + i; + testCallCount(db, "CALL spatial.layers()", null, NUM_LAYERS - i); + execute("CALL spatial.removeLayer($layerName)", map("layerName", name)); + testCallCount(db, "CALL spatial.layers()", null, NUM_LAYERS - i - 1); + } + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + @Test + public void get_and_set_feature_attributes() { + execute("CALL spatial.addWKTLayer('geom','wkt')"); + testCallCount(db, "CALL spatial.layers()", null, 1); + testCallCount(db, "CALL spatial.getFeatureAttributes('geom')", null, 0); + execute("CALL spatial.setFeatureAttributes('geom',['name','type','color'])"); + testCallCount(db, "CALL spatial.getFeatureAttributes('geom')", null, 3); + } + + @Test + public void list_spatial_procedures() { + testResult(db, "CALL spatial.procedures()", (res) -> { + Map procs = new LinkedHashMap<>(); + while (res.hasNext()) { + Map r = res.next(); + procs.put(r.get("name").toString(), r.get("signature").toString()); + } + for (String key : procs.keySet()) { + System.out.println(key + ": " + procs.get(key)); + } + assertEquals("spatial.procedures() :: (name :: STRING, signature :: STRING)", + procs.get("spatial.procedures")); + assertEquals("spatial.layers() :: (name :: STRING, signature :: STRING)", procs.get("spatial.layers")); + assertEquals("spatial.layer(name :: STRING) :: (node :: NODE)", procs.get("spatial.layer")); + assertEquals("spatial.addLayer(name :: STRING, type :: STRING, encoderConfig :: STRING) :: (node :: NODE)", + procs.get("spatial.addLayer")); + assertEquals("spatial.addNode(layerName :: STRING, node :: NODE) :: (node :: NODE)", + procs.get("spatial.addNode")); + assertEquals("spatial.addWKT(layerName :: STRING, geometry :: STRING) :: (node :: NODE)", + procs.get("spatial.addWKT")); + assertEquals("spatial.intersects(layerName :: STRING, geometry :: ANY) :: (node :: NODE)", + procs.get("spatial.intersects")); + }); + } + + @Test + public void list_layer_types() { + testResult(db, "CALL spatial.layerTypes()", (res) -> { + Map procs = new LinkedHashMap<>(); + while (res.hasNext()) { + Map r = res.next(); + procs.put(r.get("name").toString(), r.get("signature").toString()); + } + for (String key : procs.keySet()) { + System.out.println(key + ": " + procs.get(key)); + } + assertEquals( + "RegisteredLayerType(name='SimplePoint', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", + procs.get("simplepoint")); + assertEquals( + "RegisteredLayerType(name='NativePoint', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='location')", + procs.get("nativepoint")); + assertEquals( + "RegisteredLayerType(name='WKT', geometryEncoder=WKTGeometryEncoder, layerClass=EditableLayerImpl, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='geometry')", + procs.get("wkt")); + assertEquals( + "RegisteredLayerType(name='WKB', geometryEncoder=WKBGeometryEncoder, layerClass=EditableLayerImpl, index=LayerRTreeIndex, crs='WGS84(DD)', defaultConfig='geometry')", + procs.get("wkb")); + assertEquals( + "RegisteredLayerType(name='Geohash', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerGeohashPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", + procs.get("geohash")); + assertEquals( + "RegisteredLayerType(name='ZOrder', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerZOrderPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", + procs.get("zorder")); + assertEquals( + "RegisteredLayerType(name='Hilbert', geometryEncoder=SimplePointEncoder, layerClass=SimplePointLayer, index=LayerHilbertPointIndex, crs='WGS84(DD)', defaultConfig='longitude:latitude')", + procs.get("hilbert")); + assertEquals( + "RegisteredLayerType(name='NativeGeohash', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerGeohashPointIndex, crs='WGS84(DD)', defaultConfig='location')", + procs.get("nativegeohash")); + assertEquals( + "RegisteredLayerType(name='NativeZOrder', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerZOrderPointIndex, crs='WGS84(DD)', defaultConfig='location')", + procs.get("nativezorder")); + assertEquals( + "RegisteredLayerType(name='NativeHilbert', geometryEncoder=NativePointEncoder, layerClass=SimplePointLayer, index=LayerHilbertPointIndex, crs='WGS84(DD)', defaultConfig='location')", + procs.get("nativehilbert")); + }); + } + + @Test + public void find_layer() { + String wkt = "LINESTRING (15.2 60.1, 15.3 60.1)"; + execute("CALL spatial.addWKTLayer('geom','wkt')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", wkt)); + + testCall(db, "CALL spatial.layer('geom')", + (r) -> assertEquals("geom", (dump((Node) r.get("node"))).getProperty("layer"))); + testCallFails(db, "CALL spatial.layer('badname')", null, "No such layer 'badname'"); + } + + @Test + public void add_a_node_to_the_spatial_rtree_index_for_simple_points() { + execute("CALL spatial.addPointLayer('geom')"); + Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_the_spatial_geohash_index_for_simple_points() { + execute("CALL spatial.addPointLayerGeohash('geom')"); + Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_the_spatial_zorder_index_for_simple_points() { + execute("CALL spatial.addPointLayerZOrder('geom')"); + Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_the_spatial_hilbert_index_for_simple_points() { + execute("CALL spatial.addPointLayerHilbert('geom')"); + Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_multiple_different_indexes_for_both_simple_and_native_points() { + String[] encoders = new String[]{"Simple", "Native"}; + String[] indexes = new String[]{"Geohash", "ZOrder", "Hilbert", "RTree"}; + for (String encoder : encoders) { + String procName = (encoder.equalsIgnoreCase("Native")) ? "addNativePointLayer" : "addPointLayer"; + for (String indexType : indexes) { + String layerName = (encoder + indexType).toLowerCase(); + String query = + "CALL spatial." + procName + (indexType.equals("RTree") ? "" : indexType) + "('" + layerName + + "')"; + execute(query); + } + } + testResult(db, "CALL spatial.layers()", (res) -> { + while (res.hasNext()) { + Map r = res.next(); + String encoder = + r.get("name").toString().contains("native") ? "NativePointEncoder" : "SimplePointEncoder"; + MatcherAssert.assertThat("Expect simple:native encoders to appear in simple:native layers", + r.get("signature").toString(), containsString(encoder)); + } + }); + testCallCount(db, "CALL spatial.layers()", null, indexes.length * encoders.length); + Node node = createNode("CREATE (n:Node {latitude:60.1,longitude:15.2}) SET n.location=point(n) RETURN n", "n"); + for (String encoder : encoders) { + for (String indexType : indexes) { + String layerName = (encoder + indexType).toLowerCase(); + testCall(db, "MATCH (node:Node) RETURN node", r -> Assert.assertEquals(node, r.get("node"))); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('" + layerName + "',n) YIELD node RETURN node", + r -> assertEquals(node, r.get("node"))); + testCall(db, "CALL spatial.withinDistance('" + layerName + "',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + } + for (String encoder : encoders) { + for (String indexType : indexes) { + String layerName = (encoder + indexType).toLowerCase(); + execute("CALL spatial.removeLayer('" + layerName + "')"); + } + } + testCallCount(db, "CALL spatial.layers()", null, 0); + } + + + @Test + public void testDistanceNode() { + execute("CALL spatial.addPointLayer('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void testDistanceNodeWithGeohashIndex() { + execute("CALL spatial.addPointLayer('geom','geohash')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void testDistanceNodeGeohash() { + execute("CALL spatial.addPointLayerGeohash('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void testDistanceNodeZOrder() { + execute("CALL spatial.addPointLayerZOrder('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void testDistanceNodeHilbert() { + execute("CALL spatial.addPointLayerHilbert('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_the_spatial_index_short() { + execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); + Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_a_node_to_the_spatial_index_short_with_geohash() { + execute("CALL spatial.addPointLayerXY('geom','lon','lat','geohash')"); + Node node = createNode("CREATE (n:Node {lat:60.1,lon:15.2}) RETURN n", "n"); + testCall(db, "MATCH (n:Node) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + r -> Assert.assertEquals(node, r.get("node"))); + } + + @Test + public void add_two_nodes_to_the_spatial_layer() { + execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); + String node1; + String node2; + try (Transaction tx = db.beginTx()) { + Result result = tx.execute( + "CREATE (n1:Node {lat:60.1,lon:15.2}),(n2:Node {lat:60.1,lon:15.3}) WITH n1,n2 CALL spatial.addNodes('geom',[n1,n2]) YIELD count RETURN n1,n2,count"); + Map row = result.next(); + node1 = ((Node) row.get("n1")).getElementId(); + node2 = ((Node) row.get("n2")).getElementId(); + long count = (Long) row.get("count"); + Assert.assertEquals(2L, count); + result.close(); + tx.commit(); + } + testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", res -> { + assertTrue(res.hasNext()); + assertEquals(node1, ((Node) res.next().get("node")).getElementId()); + assertTrue(res.hasNext()); + assertEquals(node2, ((Node) res.next().get("node")).getElementId()); + assertFalse(res.hasNext()); + }); + try (Transaction tx = db.beginTx()) { + Node node = (Node) tx.execute("MATCH (node) WHERE elementId(node) = $nodeId RETURN node", + map("nodeId", node1)).next().get("node"); + Result removeResult = tx.execute("CALL spatial.removeNode('geom',$node) YIELD nodeId RETURN nodeId", + map("node", node)); + Assert.assertEquals(node1, removeResult.next().get("nodeId")); + removeResult.close(); + tx.commit(); + } + testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", res -> { + assertTrue(res.hasNext()); + assertEquals(node2, ((Node) res.next().get("node")).getElementId()); + assertFalse(res.hasNext()); + }); + try (Transaction tx = db.beginTx()) { + Result removeResult = tx.execute("CALL spatial.removeNode.byId('geom',$nodeId) YIELD nodeId RETURN nodeId", + map("nodeId", node2)); + Assert.assertEquals(node2, removeResult.next().get("nodeId")); + removeResult.close(); + tx.commit(); + } + testResult(db, "CALL spatial.withinDistance('geom',{lon:15.0,lat:60.0},100)", + res -> assertFalse(res.hasNext())); + } + + @Test + public void add_many_nodes_to_the_simple_point_layer_using_addNodes() { + // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison + int count = 1000; + execute("CALL spatial.addLayer('simple_poi','SimplePoint','')"); + String query = "UNWIND range(1,$count) as i\n" + + "CREATE (n:Point {id:i, latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)})\n" + + "WITH collect(n) as points\n" + + "CALL spatial.addNodes('simple_poi',points) YIELD count\n" + + "RETURN count"; + testCountQuery("addNodes", query, count, "count", map("count", count)); + testRemoveNodes("simple_poi", count); + } + + @Test + public void add_many_nodes_to_the_simple_point_layer_using_addNode() { + // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison + int count = 1000; + execute("CALL spatial.addLayer('simple_poi','SimplePoint','')"); + String query = "UNWIND range(1,$count) as i\n" + + "CREATE (n:Point {id:i, latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)})\n" + + "WITH n\n" + + "CALL spatial.addNode('simple_poi',n) YIELD node\n" + + "RETURN count(node)"; + testCountQuery("addNode", query, count, "count(node)", map("count", count)); + testRemoveNode("simple_poi", count); + } + + @Test + public void add_many_nodes_to_the_native_point_layer_using_addNodes() { + // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison + int count = 1000; + execute("CALL spatial.addLayer('native_poi','NativePoint','')"); + String query = "UNWIND range(1,$count) as i\n" + + "WITH i, Point({latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)}) AS location\n" + + "CREATE (n:Point {id: i, location:location})\n" + + "WITH collect(n) as points\n" + + "CALL spatial.addNodes('native_poi',points) YIELD count\n" + + "RETURN count"; + testCountQuery("addNodes", query, count, "count", map("count", count)); + testRemoveNodes("native_poi", count); + } + + @Test + public void add_many_nodes_to_the_native_point_layer_using_addNode() { + // Playing with this number in both tests leads to rough benchmarking of the addNode/addNodes comparison + int count = 1000; + execute("CALL spatial.addLayer('native_poi','NativePoint','')"); + String query = "UNWIND range(1,$count) as i\n" + + "WITH i, Point({latitude:(56.0+toFloat(i)/100.0),longitude:(12.0+toFloat(i)/100.0)}) AS location\n" + + "CREATE (n:Point {id: i, location:location})\n" + + "WITH n\n" + + "CALL spatial.addNode('native_poi',n) YIELD node\n" + + "RETURN count(node)"; + testCountQuery("addNode", query, count, "count(node)", map("count", count)); + testRemoveNode("native_poi", count); + } + + private void testRemoveNode(String layer, int count) { + // Check all nodes are there + testCountQuery("withinDistance", + "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", + count, "count(node)", null); + // Now remove half the points + String remove = "UNWIND range(1,$count) as i\n" + + "MATCH (n:Point {id:i})\n" + + "WITH n\n" + + "CALL spatial.removeNode('" + layer + "',n) YIELD nodeId\n" + + "RETURN count(nodeId)"; + testCountQuery("removeNode", remove, count / 2, "count(nodeId)", map("count", count / 2)); + // Check that only half remain + testCountQuery("withinDistance", + "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", + count / 2, "count(node)", null); + } + + private void testRemoveNodes(String layer, int count) { + // Check all nodes are there + testCountQuery("withinDistance", + "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", + count, "count(node)", null); + // Now remove half the points + String remove = "UNWIND range(1,$count) as i\n" + + "MATCH (n:Point {id:i})\n" + + "WITH collect(n) as points\n" + + "CALL spatial.removeNodes('" + layer + "',points) YIELD count\n" + + "RETURN count"; + testCountQuery("removeNodes", remove, count / 2, "count", map("count", count / 2)); + // Check that only half remain + testCountQuery("withinDistance", + "CALL spatial.withinDistance('" + layer + "',{lon:15.0,lat:60.0},1000) YIELD node RETURN count(node)", + count / 2, "count(node)", null); + } + + @Test + public void import_shapefile() { + testCountQuery("importShapefile", "CALL spatial.importShapefile('shp/highway.shp')", 143, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_shapefile_without_extension() { + testCountQuery("importShapefile", "CALL spatial.importShapefile('shp/highway')", 143, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_shapefile_to_layer() { + execute("CALL spatial.addWKTLayer('geom','wkt')"); + testCountQuery("importShapefileToLayer", "CALL spatial.importShapefileToLayer('geom','shp/highway.shp')", 143, + "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm() { + testCountQuery("importOSM", "CALL spatial.importOSM('map.osm')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_twice_should_fail() { + testCountQuery("importOSM", "CALL spatial.importOSM('map.osm')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + testCallFails(db, "CALL spatial.importOSM('map.osm')", null, "Layer already exists: 'map.osm'"); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_without_extension() { + testCountQuery("importOSM", "CALL spatial.importOSM('map')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_to_layer() { + execute("CALL spatial.addLayer('geom','OSM','')"); + testCountQuery("importOSMToLayer", "CALL spatial.importOSMToLayer('geom','map.osm')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_twice_should_pass_with_different_layers() { + execute("CALL spatial.addLayer('geom1','OSM','')"); + execute("CALL spatial.addLayer('geom2','OSM','')"); + + testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom1','map.osm')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 2); + testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, + 217); + testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, 0); + + testCountQuery("importOSM", "CALL spatial.importOSMToLayer('geom2','map.osm')", 55, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 2); + testCallCount(db, "CALL spatial.withinDistance('geom1',{lon:6.3740429666,lat:50.93676351666},10000)", null, + 217); + testCallCount(db, "CALL spatial.withinDistance('geom2',{lon:6.3740429666,lat:50.93676351666},10000)", null, + 217); + } + + @Ignore + public void import_cracow_to_layer() { + execute("CALL spatial.addLayer('geom','OSM','')"); + testCountQuery("importCracowToLayer", "CALL spatial.importOSMToLayer('geom','issue-347/cra.osm')", 256253, + "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_to_layer_without_changesets() { + execute("CALL spatial.addLayer('osm_example','OSM','')"); + testCountQuery("importOSMToLayerWithoutChangesets", "CALL spatial.importOSMToLayer('osm_example','sample.osm')", + 1, "count", null); + testCallCount(db, "CALL spatial.layers()", null, 1); + } + + @Test + public void import_osm_and_add_geometry() { + execute("CALL spatial.addLayer('geom','OSM','')"); + testCountQuery("importOSMToLayerAndAddGeometry", "CALL spatial.importOSMToLayer('geom','map.osm')", 55, "count", + null); + testCallCount(db, "CALL spatial.layers()", null, 1); + testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", null, 0); + testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},10000)", null, 217); + + // Adding a point to the layer + Node node = createNode( + "CALL spatial.addWKT('geom', 'POINT(6.3740429666 50.93676351666)') YIELD node RETURN node", "node"); + testCall(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", + r -> assertEquals(node, r.get("node"))); + testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},100)", null, 1); + testCallCount(db, "CALL spatial.withinDistance('geom',{lon:6.3740429666,lat:50.93676351666},10000)", null, 218); + } + + @Test + public void import_osm_and_polygons_withinDistance() { + Map params = map("osmFile", "withinDistance.osm", "busShelterID", 2938842290L); + execute("CALL spatial.addLayer('geom','OSM','')"); + testCountQuery("importOSMAndPolygonsWithinDistance", "CALL spatial.importOSMToLayer('geom',$osmFile)", 74, + "count", params); + testCallCount(db, "CALL spatial.layers()", null, 1); + testCallCount(db, + "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance RETURN node, distance", + params, 516); + testResult(db, + "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance WITH node, distance ORDER BY distance LIMIT 20 MATCH (node)<-[:GEOM]-(osmNode) RETURN node, distance, osmNode, properties(osmNode) as props", + params, res -> { + while (res.hasNext()) { + Map r = res.next(); + assertThat("Result should have 'node'", r, hasKey("node")); + assertThat("Result should have 'distance'", r, hasKey("distance")); + assertThat("Result should have 'osmNode'", r, hasKey("osmNode")); + assertThat("Result should have 'props'", r, hasKey("props")); + Node node = (Node) r.get("node"); + double distance = (Double) r.get("distance"); + Node osmNode = (Node) r.get("osmNode"); + @SuppressWarnings("rawtypes") Map props = (Map) r.get("props"); + System.out.println( + "(node[" + node.getElementId() + "])<-[:GEOM {distance:" + distance + "}]-(osmNode[" + + osmNode.getElementId() + "] " + props + ") "); + assertThat("Node should have either way_osm_id or node_osm_id", props, + anyOf(hasKey("node_osm_id"), hasKey("way_osm_id"))); + } + }); + testResult(db, + "MATCH (n) WHERE n.node_osm_id = $busShelterID CALL spatial.withinDistance('geom',n,100) YIELD node, distance WITH node, distance ORDER BY distance LIMIT 20 MATCH (n) WHERE elementId(n)=elementId(node) RETURN node, distance, spatial.decodeGeometry('geom',n) AS geometry", + params, res -> { + while (res.hasNext()) { + Map r = res.next(); + assertThat("Result should have 'node'", r, hasKey("node")); + assertThat("Result should have 'distance'", r, hasKey("distance")); + assertThat("Result should have 'geometry'", r, hasKey("geometry")); + Node node = (Node) r.get("node"); + double distance = (Double) r.get("distance"); + Object geometry = r.get("geometry"); + System.out.println(node.toString() + " at " + distance + ": " + geometry); + if (geometry instanceof Point) { + assertThat("Point has 2D coordinates", + ((Point) geometry).getCoordinate().getCoordinate().length, equalTo(2)); + } else if (geometry instanceof Map) { + Map map = (Map) geometry; + assertThat("Geometry should contain a type", map, hasKey("type")); + assertThat("Geometry should contain coordinates", map, hasKey("coordinates")); + assertThat("Geometry should not be a point", map.get("type"), not(equalTo("Point"))); + } else { + fail("Geometry should be either a point or a Map containing coordinates"); + } + } + }); + } + + private void testCountQuery(String name, String query, long count, String column, Map params) { + // warmup + try (Transaction tx = db.beginTx()) { + Result results = tx.execute("EXPLAIN " + query, params == null ? map() : params); + results.close(); + tx.commit(); + } + long start = System.currentTimeMillis(); + testResult(db, query, params, res -> { + assertTrue("Expected a single result", res.hasNext()); + long c = (Long) res.next().get(column); + assertFalse("Expected a single result", res.hasNext()); + Assert.assertEquals("Expected count of " + count + " nodes but got " + c, count, c); + } + ); + System.out.println(name + " query took " + (System.currentTimeMillis() - start) + "ms - " + params); + } + + @Test + public void find_geometries_in_a_bounding_box_short() { + execute("CALL spatial.addPointLayerXY('geom','lon','lat')"); + Node node = createNode( + "CREATE (n:Node {lat:60.1,lon:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void find_geometries_in_a_bounding_box() { + execute("CALL spatial.addPointLayer('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void find_geometries_in_a_polygon() { + execute("CALL spatial.addPointLayer('geom')"); + executeWrite( + "UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); + String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; + testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", + r -> assertEquals("b", r.get("name"))); + } + + @Test + public void find_geometries_in_a_bounding_box_geohash() { + execute("CALL spatial.addPointLayerGeohash('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void find_geometries_in_a_polygon_geohash() { + execute("CALL spatial.addPointLayerGeohash('geom')"); + executeWrite( + "UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); + String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; + testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", + r -> assertEquals("b", r.get("name"))); + } + + @Test + public void find_geometries_in_a_bounding_box_zorder() { + execute("CALL spatial.addPointLayerZOrder('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void find_geometries_in_a_polygon_zorder() { + execute("CALL spatial.addPointLayerZOrder('geom')"); + executeWrite( + "UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); + String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; + testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", + r -> assertEquals("b", r.get("name"))); + } + + @Test + public void find_geometries_in_a_bounding_box_hilbert() { + execute("CALL spatial.addPointLayerHilbert('geom')"); + Node node = createNode( + "CREATE (n:Node {latitude:60.1,longitude:15.2}) WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node", + "node"); + testCall(db, "CALL spatial.bbox('geom',{lon:15.0,lat:60.0}, {lon:15.3, lat:61.0})", + r -> assertEquals(node, r.get("node"))); + } + + @Test + public void find_geometries_in_a_polygon_hilbert() { + execute("CALL spatial.addPointLayerHilbert('geom')"); + executeWrite( + "UNWIND [{name:'a',latitude:60.1,longitude:15.2},{name:'b',latitude:60.3,longitude:15.5}] as point CREATE (n:Node) SET n += point WITH n CALL spatial.addNode('geom',n) YIELD node RETURN node.name as name"); + String polygon = "POLYGON((15.3 60.2, 15.3 60.4, 15.7 60.4, 15.7 60.2, 15.3 60.2))"; + testCall(db, "CALL spatial.intersects('geom','" + polygon + "') YIELD node RETURN node.name as name", + r -> assertEquals("b", r.get("name"))); + } + + @Test + public void create_a_WKT_layer() { + testCall(db, "CALL spatial.addWKTLayer('geom','wkt')", + r -> assertEquals("wkt", dump(((Node) r.get("node"))).getProperty("geomencoder_config"))); + } + + private static Node dump(Node n) { + System.out.printf("id %s props %s%n", n.getElementId(), n.getAllProperties()); + System.out.flush(); + return n; + } + + @Test + public void add_a_WKT_geometry_to_a_layer() { + String lineString = "LINESTRING (15.2 60.1, 15.3 60.1)"; + + execute("CALL spatial.addWKTLayer('geom','wkt')"); + testCall(db, "CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString), + r -> assertEquals(lineString, dump(((Node) r.get("node"))).getProperty("wkt"))); + } + + @Test + public void find_geometries_close_to_a_point_wkt() { + String lineString = "LINESTRING (15.2 60.1, 15.3 60.1)"; + execute("CALL spatial.addLayer('geom','WKT','wkt')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); + testCall(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", + r -> assertEquals(lineString, (dump((Node) r.get("node"))).getProperty("wkt"))); + } + + @Test + public void find_geometries_close_to_a_point_geohash() { + String lineString = "POINT (15.2 60.1)"; + execute("CALL spatial.addLayer('geom','geohash','lon:lat')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); + testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); + } + + @Test + public void find_geometries_close_to_a_point_zorder() { + String lineString = "POINT (15.2 60.1)"; + execute("CALL spatial.addLayer('geom','zorder','lon:lat')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); + testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); + } + + @Test + public void find_geometries_close_to_a_point_hilbert() { + String lineString = "POINT (15.2 60.1)"; + execute("CALL spatial.addLayer('geom','hilbert','lon:lat')"); + execute("CALL spatial.addWKT('geom',$wkt)", map("wkt", lineString)); + testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 1); + } + + @Test + public void find_no_geometries_using_closest_on_empty_layer() { + execute("CALL spatial.addLayer('geom','WKT','wkt')"); + testCallCount(db, "CALL spatial.closest('geom',{lon:15.2, lat:60.1}, 1.0)", null, 0); + } /* diff --git a/src/test/java/org/neo4j/gis/spatial/rtree/EnvelopeTests.java b/src/test/java/org/neo4j/gis/spatial/rtree/EnvelopeTests.java index 293ff8a89..e63799a8a 100644 --- a/src/test/java/org/neo4j/gis/spatial/rtree/EnvelopeTests.java +++ b/src/test/java/org/neo4j/gis/spatial/rtree/EnvelopeTests.java @@ -19,109 +19,112 @@ */ package org.neo4j.gis.spatial.rtree; -import org.junit.jupiter.api.Test; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + public class EnvelopeTests { - @Test - public void shouldCreateBasic2DEnvelopes() { - for (double width = 0.0; width < 10.0; width += 2.5) { - for (double minx = -10.0; minx < 10.0; minx += 2.5) { - for (double miny = -10.0; miny < 10.0; miny += 2.5) { - double maxx = minx + width; - double maxy = miny + width; - makeAndTestEnvelope(new double[]{minx, miny}, new double[]{maxx, maxy}, new double[]{width, width}); - } - } - } - } + @Test + public void shouldCreateBasic2DEnvelopes() { + for (double width = 0.0; width < 10.0; width += 2.5) { + for (double minx = -10.0; minx < 10.0; minx += 2.5) { + for (double miny = -10.0; miny < 10.0; miny += 2.5) { + double maxx = minx + width; + double maxy = miny + width; + makeAndTestEnvelope(new double[]{minx, miny}, new double[]{maxx, maxy}, new double[]{width, width}); + } + } + } + } - @Test - public void shouldHandleIntersectionsIn1D() { - double width_x = 1.0; - double width_y = 1.0; - Envelope left = new Envelope(0.0, width_x, 0.0, width_y); - for (double minx = -10.0; minx < 10.0; minx += 0.2) { - double maxx = minx + width_x; - Envelope right = new Envelope(minx, maxx, 0.0, width_y); - if (maxx < left.getMinX() || minx > left.getMaxX()) { - testDoesNotOverlap(left, right); - } else { - double overlap_x = (maxx < left.getMaxX()) ? maxx - left.getMinX() : left.getMaxX() - minx; - double overlap = overlap_x * width_y; - testOverlaps(left, right, true, overlap); - } - } - } + @Test + public void shouldHandleIntersectionsIn1D() { + double width_x = 1.0; + double width_y = 1.0; + Envelope left = new Envelope(0.0, width_x, 0.0, width_y); + for (double minx = -10.0; minx < 10.0; minx += 0.2) { + double maxx = minx + width_x; + Envelope right = new Envelope(minx, maxx, 0.0, width_y); + if (maxx < left.getMinX() || minx > left.getMaxX()) { + testDoesNotOverlap(left, right); + } else { + double overlap_x = (maxx < left.getMaxX()) ? maxx - left.getMinX() : left.getMaxX() - minx; + double overlap = overlap_x * width_y; + testOverlaps(left, right, true, overlap); + } + } + } - private void testDoesNotOverlap(Envelope left, Envelope right) { - Envelope bbox = new Envelope(left); - bbox.expandToInclude(right); - testOverlaps(left, right, false, 0.0); - } + private void testDoesNotOverlap(Envelope left, Envelope right) { + Envelope bbox = new Envelope(left); + bbox.expandToInclude(right); + testOverlaps(left, right, false, 0.0); + } - private void testOverlaps(Envelope left, Envelope right, boolean intersects, double overlap) { - String intersectMessage = intersects ? "Should intersect" : "Should not intersect"; - String overlapMessage = intersects ? "Should overlap" : "Should not have overlap"; - assertThat(intersectMessage, left.intersects(right), equalTo(intersects)); - assertThat(intersectMessage, right.intersects(left), equalTo(intersects)); - assertThat(overlapMessage, left.overlap(right), closeTo(overlap, 0.000001)); - assertThat(overlapMessage, right.overlap(left), closeTo(overlap, 0.000001)); - } + private void testOverlaps(Envelope left, Envelope right, boolean intersects, double overlap) { + String intersectMessage = intersects ? "Should intersect" : "Should not intersect"; + String overlapMessage = intersects ? "Should overlap" : "Should not have overlap"; + assertThat(intersectMessage, left.intersects(right), equalTo(intersects)); + assertThat(intersectMessage, right.intersects(left), equalTo(intersects)); + assertThat(overlapMessage, left.overlap(right), closeTo(overlap, 0.000001)); + assertThat(overlapMessage, right.overlap(left), closeTo(overlap, 0.000001)); + } - @SuppressWarnings("SameParameterValue") - private void testOverlaps(Envelope left, Envelope right, boolean intersects, double overlap, double overlapArea, double bboxArea) { - testOverlaps(left, right, intersects, overlap); - assertThat("Expected overlap area", left.intersection(right).getArea(), closeTo(overlapArea, 0.000001)); - assertThat("Expected overlap area", right.intersection(left).getArea(), closeTo(overlapArea, 0.000001)); - assertThat("Expected bbox area", left.bbox(right).getArea(), closeTo(bboxArea, 0.000001)); - assertThat("Expected bbox area", right.bbox(left).getArea(), closeTo(bboxArea, 0.000001)); - } + @SuppressWarnings("SameParameterValue") + private void testOverlaps(Envelope left, Envelope right, boolean intersects, double overlap, double overlapArea, + double bboxArea) { + testOverlaps(left, right, intersects, overlap); + assertThat("Expected overlap area", left.intersection(right).getArea(), closeTo(overlapArea, 0.000001)); + assertThat("Expected overlap area", right.intersection(left).getArea(), closeTo(overlapArea, 0.000001)); + assertThat("Expected bbox area", left.bbox(right).getArea(), closeTo(bboxArea, 0.000001)); + assertThat("Expected bbox area", right.bbox(left).getArea(), closeTo(bboxArea, 0.000001)); + } - @Test - public void shouldHandleIntersectionsIn2D() { - Envelope left = new Envelope(0.0, 1.0, 0.0, 1.0); - testOverlaps(left, new Envelope(0.0, 1.0, 0.0, 1.0), true, 1.0, 1.0, 1.0); // copies - testOverlaps(left, new Envelope(0.5, 1.0, 0.5, 1.0), true, 1.0, 0.25, 1.0); // top right quadrant - testOverlaps(left, new Envelope(0.25, 0.75, 0.25, 0.75), true, 1.0, 0.25, 1.0); // centered - testOverlaps(left, new Envelope(-0.5, 0.5, -0.5, 0.5), true, 0.25, 0.25, 2.25); // overlaps bottom left quadrant - testOverlaps(left, new Envelope(-0.5, 1.5, -0.5, 1.5), true, 1.0, 1.0, 4.0); // encapsulates - testOverlaps(left, new Envelope(-1.0, 0.0, 0.0, 1.0), true, 0.0, 0.0, 2.0); // touches left edge - testOverlaps(left, new Envelope(0.5, 1.5, 1.0, 2.0), true, 0.0, 0.0, 3.0); // touches top-right edge - testOverlaps(left, new Envelope(0.5, 1.5, 0.0, 1.0), true, 0.5, 0.5, 1.5); // overlaps right half - } + @Test + public void shouldHandleIntersectionsIn2D() { + Envelope left = new Envelope(0.0, 1.0, 0.0, 1.0); + testOverlaps(left, new Envelope(0.0, 1.0, 0.0, 1.0), true, 1.0, 1.0, 1.0); // copies + testOverlaps(left, new Envelope(0.5, 1.0, 0.5, 1.0), true, 1.0, 0.25, 1.0); // top right quadrant + testOverlaps(left, new Envelope(0.25, 0.75, 0.25, 0.75), true, 1.0, 0.25, 1.0); // centered + testOverlaps(left, new Envelope(-0.5, 0.5, -0.5, 0.5), true, 0.25, 0.25, + 2.25); // overlaps bottom left quadrant + testOverlaps(left, new Envelope(-0.5, 1.5, -0.5, 1.5), true, 1.0, 1.0, 4.0); // encapsulates + testOverlaps(left, new Envelope(-1.0, 0.0, 0.0, 1.0), true, 0.0, 0.0, 2.0); // touches left edge + testOverlaps(left, new Envelope(0.5, 1.5, 1.0, 2.0), true, 0.0, 0.0, 3.0); // touches top-right edge + testOverlaps(left, new Envelope(0.5, 1.5, 0.0, 1.0), true, 0.5, 0.5, 1.5); // overlaps right half + } - private void makeAndTestEnvelope(double[] min, double[] max, double[] width) { - Envelope env = new Envelope(min, max); - assertThat("Expected min-x to be correct", env.getMinX(), equalTo(min[0])); - assertThat("Expected min-y to be correct", env.getMinY(), equalTo(min[1])); - assertThat("Expected max-x to be correct", env.getMaxX(), equalTo(max[0])); - assertThat("Expected max-y to be correct", env.getMaxY(), equalTo(max[1])); - assertThat("Expected dimension to be same as min.length", env.getDimension(), equalTo(min.length)); - assertThat("Expected dimension to be same as max.length", env.getDimension(), equalTo(max.length)); - for (int i = 0; i < min.length; i++) { - assertThat("Expected min[" + i + "] to be correct", env.getMin(i), equalTo(min[i])); - assertThat("Expected max[" + i + "] to be correct", env.getMax(i), equalTo(max[i])); - } - double area = 1.0; - Envelope copy = new Envelope(env); - Envelope intersection = env.intersection(copy); - for (int i = 0; i < min.length; i++) { - assertThat("Expected width[" + i + "] to be correct", env.getWidth(i), equalTo(width[i])); - assertThat("Expected copied width[" + i + "] to be correct", copy.getWidth(i), equalTo(width[i])); - assertThat("Expected intersected width[" + i + "] to be correct", intersection.getWidth(i), equalTo(width[i])); - area *= width[i]; - } - assertThat("Expected area to be correct", env.getArea(), equalTo(area)); - assertThat("Expected copied area to be correct", env.getArea(), equalTo(copy.getArea())); - assertThat("Expected intersected area to be correct", env.getArea(), equalTo(intersection.getArea())); - assertTrue(env.intersects(copy), "Expected copied envelope to intersect"); - assertThat("Expected copied envelope to intersect completely", env.overlap(copy), equalTo(1.0)); - } + private void makeAndTestEnvelope(double[] min, double[] max, double[] width) { + Envelope env = new Envelope(min, max); + assertThat("Expected min-x to be correct", env.getMinX(), equalTo(min[0])); + assertThat("Expected min-y to be correct", env.getMinY(), equalTo(min[1])); + assertThat("Expected max-x to be correct", env.getMaxX(), equalTo(max[0])); + assertThat("Expected max-y to be correct", env.getMaxY(), equalTo(max[1])); + assertThat("Expected dimension to be same as min.length", env.getDimension(), equalTo(min.length)); + assertThat("Expected dimension to be same as max.length", env.getDimension(), equalTo(max.length)); + for (int i = 0; i < min.length; i++) { + assertThat("Expected min[" + i + "] to be correct", env.getMin(i), equalTo(min[i])); + assertThat("Expected max[" + i + "] to be correct", env.getMax(i), equalTo(max[i])); + } + double area = 1.0; + Envelope copy = new Envelope(env); + Envelope intersection = env.intersection(copy); + for (int i = 0; i < min.length; i++) { + assertThat("Expected width[" + i + "] to be correct", env.getWidth(i), equalTo(width[i])); + assertThat("Expected copied width[" + i + "] to be correct", copy.getWidth(i), equalTo(width[i])); + assertThat("Expected intersected width[" + i + "] to be correct", intersection.getWidth(i), + equalTo(width[i])); + area *= width[i]; + } + assertThat("Expected area to be correct", env.getArea(), equalTo(area)); + assertThat("Expected copied area to be correct", env.getArea(), equalTo(copy.getArea())); + assertThat("Expected intersected area to be correct", env.getArea(), equalTo(intersection.getArea())); + assertTrue(env.intersects(copy), "Expected copied envelope to intersect"); + assertThat("Expected copied envelope to intersect completely", env.overlap(copy), equalTo(1.0)); + } } diff --git a/src/test/java/org/neo4j/gis/spatial/rtree/RTreeTests.java b/src/test/java/org/neo4j/gis/spatial/rtree/RTreeTests.java index 26f80a802..5a84fd3cd 100644 --- a/src/test/java/org/neo4j/gis/spatial/rtree/RTreeTests.java +++ b/src/test/java/org/neo4j/gis/spatial/rtree/RTreeTests.java @@ -19,6 +19,13 @@ */ package org.neo4j.gis.spatial.rtree; +import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import org.geotools.api.feature.simple.SimpleFeatureType; import org.geotools.data.neo4j.Neo4jFeatureBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -31,116 +38,112 @@ import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Transaction; import org.neo4j.test.TestDatabaseManagementServiceBuilder; -import org.geotools.api.feature.simple.SimpleFeatureType; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; - -import static org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME; public class RTreeTests { - private static final boolean exportImages = false; // TODO: This can be enabled once we port to newer GeoTools that works with Java11 - private DatabaseManagementService databases; - private GraphDatabaseService db; - private TestRTreeIndex rtree; - private RTreeImageExporter imageExporter; - @BeforeEach - public void setup() { - databases = new TestDatabaseManagementServiceBuilder(Path.of("target", "rtree")).impermanent().build(); - db = databases.database(DEFAULT_DATABASE_NAME); - try (Transaction tx = db.beginTx()) { - this.rtree = new TestRTreeIndex(tx); - tx.commit(); - } - if (exportImages) { - SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("test", Constants.GTYPE_POINT, null, new String[]{}); - imageExporter = new RTreeImageExporter(new GeometryFactory(), new SimplePointEncoder(), null, featureType, rtree); - try (Transaction tx = db.beginTx()) { - imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); - tx.commit(); - } - } - } + private static final boolean exportImages = false; // TODO: This can be enabled once we port to newer GeoTools that works with Java11 + private DatabaseManagementService databases; + private GraphDatabaseService db; + private TestRTreeIndex rtree; + private RTreeImageExporter imageExporter; + + @BeforeEach + public void setup() { + databases = new TestDatabaseManagementServiceBuilder(Path.of("target", "rtree")).impermanent().build(); + db = databases.database(DEFAULT_DATABASE_NAME); + try (Transaction tx = db.beginTx()) { + this.rtree = new TestRTreeIndex(tx); + tx.commit(); + } + if (exportImages) { + SimpleFeatureType featureType = Neo4jFeatureBuilder.getType("test", Constants.GTYPE_POINT, null, + new String[]{}); + imageExporter = new RTreeImageExporter(new GeometryFactory(), new SimplePointEncoder(), null, featureType, + rtree); + try (Transaction tx = db.beginTx()) { + imageExporter.initialize(tx, new Coordinate(0.0, 0.0), new Coordinate(1.0, 1.0)); + tx.commit(); + } + } + } - @AfterEach - public void teardown() { - databases.shutdown(); - } + @AfterEach + public void teardown() { + databases.shutdown(); + } - @Test - public void shouldMergeTwoPartiallyOverlappingTrees() throws IOException { - RTreeIndex.NodeWithEnvelope rootLeft; - RTreeIndex.NodeWithEnvelope rootRight; - try (Transaction tx = db.beginTx()) { - rootLeft = createSimpleRTree(0.01, 0.81, 5); - tx.commit(); - } - try (Transaction tx = db.beginTx()) { - rootRight = createSimpleRTree(0.19, 0.99, 5); - tx.commit(); - } - System.out.println("Created two trees"); - if (exportImages) { - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-left.png"), rootLeft.node, 7); - imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-right.png"), rootRight.node, 7); - tx.commit(); - } - } - try (Transaction tx = db.beginTx()) { - rtree.mergeTwoTrees(tx, rootLeft.refresh(tx), rootRight.refresh(tx)); - tx.commit(); - } - System.out.println("Merged two trees"); - if (exportImages) { - try (Transaction tx = db.beginTx()) { - imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-merged.png"), rootLeft.node, 7); - tx.commit(); - } - } - } + @Test + public void shouldMergeTwoPartiallyOverlappingTrees() throws IOException { + RTreeIndex.NodeWithEnvelope rootLeft; + RTreeIndex.NodeWithEnvelope rootRight; + try (Transaction tx = db.beginTx()) { + rootLeft = createSimpleRTree(0.01, 0.81, 5); + tx.commit(); + } + try (Transaction tx = db.beginTx()) { + rootRight = createSimpleRTree(0.19, 0.99, 5); + tx.commit(); + } + System.out.println("Created two trees"); + if (exportImages) { + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-left.png"), rootLeft.node, 7); + imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-right.png"), rootRight.node, 7); + tx.commit(); + } + } + try (Transaction tx = db.beginTx()) { + rtree.mergeTwoTrees(tx, rootLeft.refresh(tx), rootRight.refresh(tx)); + tx.commit(); + } + System.out.println("Merged two trees"); + if (exportImages) { + try (Transaction tx = db.beginTx()) { + imageExporter.saveRTreeLayers(tx, new File("target/rtree-test/rtree-merged.png"), rootLeft.node, 7); + tx.commit(); + } + } + } - @SuppressWarnings("SameParameterValue") - private RTreeIndex.NodeWithEnvelope createSimpleRTree(double minx, double maxx, int depth) { - double[] min = new double[]{minx, minx}; - double[] max = new double[]{maxx, maxx}; - try (Transaction tx = db.beginTx()) { - RTreeIndex.NodeWithEnvelope rootNode = new RTreeIndex.NodeWithEnvelope(tx.createNode(), new Envelope(min, max)); - rtree.setIndexNodeEnvelope(rootNode); - ArrayList parents = new ArrayList<>(); - ArrayList children = new ArrayList<>(); - parents.add(rootNode); - for (int i = 1; i < depth; i++) { - for (RTreeIndex.NodeWithEnvelope parent : parents) { - Envelope[] envs = new Envelope[]{ - makeEnvelope(parent.envelope, 0.5, 0.0, 0.0), - makeEnvelope(parent.envelope, 0.5, 1.0, 0.0), - makeEnvelope(parent.envelope, 0.5, 1.0, 1.0), - makeEnvelope(parent.envelope, 0.5, 0.0, 1.0) - }; - for (Envelope env : envs) { - RTreeIndex.NodeWithEnvelope child = rtree.makeChildIndexNode(tx, parent, env); - children.add(child); - } - } - parents.clear(); - parents.addAll(children); - children.clear(); - } - tx.commit(); - return rootNode; - } - } + @SuppressWarnings("SameParameterValue") + private RTreeIndex.NodeWithEnvelope createSimpleRTree(double minx, double maxx, int depth) { + double[] min = new double[]{minx, minx}; + double[] max = new double[]{maxx, maxx}; + try (Transaction tx = db.beginTx()) { + RTreeIndex.NodeWithEnvelope rootNode = new RTreeIndex.NodeWithEnvelope(tx.createNode(), + new Envelope(min, max)); + rtree.setIndexNodeEnvelope(rootNode); + ArrayList parents = new ArrayList<>(); + ArrayList children = new ArrayList<>(); + parents.add(rootNode); + for (int i = 1; i < depth; i++) { + for (RTreeIndex.NodeWithEnvelope parent : parents) { + Envelope[] envs = new Envelope[]{ + makeEnvelope(parent.envelope, 0.5, 0.0, 0.0), + makeEnvelope(parent.envelope, 0.5, 1.0, 0.0), + makeEnvelope(parent.envelope, 0.5, 1.0, 1.0), + makeEnvelope(parent.envelope, 0.5, 0.0, 1.0) + }; + for (Envelope env : envs) { + RTreeIndex.NodeWithEnvelope child = rtree.makeChildIndexNode(tx, parent, env); + children.add(child); + } + } + parents.clear(); + parents.addAll(children); + children.clear(); + } + tx.commit(); + return rootNode; + } + } - @SuppressWarnings("SameParameterValue") - Envelope makeEnvelope(Envelope parent, double scaleFactor, double offsetX, double offsetY) { - Envelope env = new Envelope(parent); - env.scaleBy(scaleFactor); - env.shiftBy(offsetX * env.getWidth(0), 0); - env.shiftBy(offsetY * env.getWidth(1), 1); - return env; - } + @SuppressWarnings("SameParameterValue") + Envelope makeEnvelope(Envelope parent, double scaleFactor, double offsetX, double offsetY) { + Envelope env = new Envelope(parent); + env.scaleBy(scaleFactor); + env.shiftBy(offsetX * env.getWidth(0), 0); + env.shiftBy(offsetY * env.getWidth(1), 1); + return env; + } } diff --git a/src/test/java/org/neo4j/gis/spatial/rtree/TestRTreeIndex.java b/src/test/java/org/neo4j/gis/spatial/rtree/TestRTreeIndex.java index e377ff1b7..2be168a56 100644 --- a/src/test/java/org/neo4j/gis/spatial/rtree/TestRTreeIndex.java +++ b/src/test/java/org/neo4j/gis/spatial/rtree/TestRTreeIndex.java @@ -25,24 +25,25 @@ public class TestRTreeIndex extends RTreeIndex { - // TODO: Rather pass tx into init after construction (bad pattern to pass tx to constructor, as if it will be saved) - public TestRTreeIndex(Transaction tx) { - init(tx, tx.createNode(), new SimplePointEncoder(), DEFAULT_MAX_NODE_REFERENCES); - } + // TODO: Rather pass tx into init after construction (bad pattern to pass tx to constructor, as if it will be saved) + public TestRTreeIndex(Transaction tx) { + init(tx, tx.createNode(), new SimplePointEncoder(), DEFAULT_MAX_NODE_REFERENCES); + } - public RTreeIndex.NodeWithEnvelope makeChildIndexNode(Transaction tx, NodeWithEnvelope parent, Envelope bbox) { - Node indexNode = tx.createNode(); - setIndexNodeEnvelope(indexNode, bbox); - parent.node.createRelationshipTo(indexNode, RTreeRelationshipTypes.RTREE_CHILD); - expandParentBoundingBoxAfterNewChild(parent.node, new double[]{bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY()}); - return new NodeWithEnvelope(indexNode, bbox); - } + public RTreeIndex.NodeWithEnvelope makeChildIndexNode(Transaction tx, NodeWithEnvelope parent, Envelope bbox) { + Node indexNode = tx.createNode(); + setIndexNodeEnvelope(indexNode, bbox); + parent.node.createRelationshipTo(indexNode, RTreeRelationshipTypes.RTREE_CHILD); + expandParentBoundingBoxAfterNewChild(parent.node, + new double[]{bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY()}); + return new NodeWithEnvelope(indexNode, bbox); + } - public void setIndexNodeEnvelope(NodeWithEnvelope indexNode) { - setIndexNodeEnvelope(indexNode.node, indexNode.envelope); - } + public void setIndexNodeEnvelope(NodeWithEnvelope indexNode) { + setIndexNodeEnvelope(indexNode.node, indexNode.envelope); + } - public void mergeTwoTrees(Transaction tx, NodeWithEnvelope left, NodeWithEnvelope right) { - super.mergeTwoSubtrees(tx, left, this.getIndexChildren(right.node)); - } + public void mergeTwoTrees(Transaction tx, NodeWithEnvelope left, NodeWithEnvelope right) { + super.mergeTwoSubtrees(tx, left, this.getIndexChildren(right.node)); + } }