From f36c43c244613569a3a043e6bfdbc9609a510592 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sat, 4 Feb 2017 12:15:19 -0800 Subject: [PATCH 01/12] Make rake task available outside gem --- README.md | 6 +++ Rakefile | 38 +--------------- lib/neo4jrb_spatial/rake_tasks.rb | 2 + .../rake_tasks/neo4j_spatial.rake | 44 +++++++++++++++++++ 4 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 lib/neo4jrb_spatial/rake_tasks.rb create mode 100644 lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake diff --git a/README.md b/README.md index 7d30419..34f0239 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ It then drops you back into a QueryProxy in the context of the class. If you had If you did no define `spatial_index` on your model or what to query against something other than the model's default, you can feed a third argument: the index to use for the query. +## Rake tasks: +#### `neo4j_spatial:install` + +usage: `NEO4J_VERSION='3.0.4' rake neo4j_spatial:install[]` +If no `env` argument is provided, this defaults to 'development' + ## Additional Resources Check out the specs and the code for help, it's rather straightforward. diff --git a/Rakefile b/Rakefile index c6f2a99..60c71c1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,45 +1,9 @@ require 'bundler/gem_tasks' require 'neo4j/rake_tasks' -require 'net/http' - -def system_or_fail(command) - system(command) || exit(1) -end +require 'neo4jrb_spatial/rake_tasks' task 'spec' do system_or_fail('rspec spec') end -namespace :neo4j_spatial do - def match_version?(version, max_version) - min_version = max_version.split('.')[0..-1].join('.') - Gem::Version.new(version) <= Gem::Version.new(max_version) && - Gem::Version.new(version) >= Gem::Version.new(min_version) - end - - def matching_version(version) - uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml' - versions = Net::HTTP.get_response(URI.parse(uri)).body - versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) - versions.map! { |e| e.first.split('-neo4j-') } - versions.select { |e| match_version?(version, e.last) }.last - end - - task 'install' do - url = 'https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial' - input_version = ENV['NEO4J_VERSION'] - fail ArgumentError, 'Missing NEO4J_VERSION' unless input_version - spatial_version, neo4j_version = *matching_version(input_version) - if neo4j_version[0].to_i < 3 - file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.zip" - system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true") - system_or_fail("unzip #{file_name} -d ./db/neo4j/development/plugins") - else - file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.jar" - system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true") - system_or_fail("mv #{file_name} ./db/neo4j/development/plugins") - end - end -end - task default: ['spec'] diff --git a/lib/neo4jrb_spatial/rake_tasks.rb b/lib/neo4jrb_spatial/rake_tasks.rb new file mode 100644 index 0000000..91b758b --- /dev/null +++ b/lib/neo4jrb_spatial/rake_tasks.rb @@ -0,0 +1,2 @@ +require 'rake' +load 'neo4jrb_spatial/rake_tasks/neo4j_spatial.rake' diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake new file mode 100644 index 0000000..13a6323 --- /dev/null +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -0,0 +1,44 @@ +require 'net/http' + +def system_or_fail(command) + system(command) || exit(1) +end + +namespace :neo4j_spatial do + def match_version?(version, max_version) + min_version = max_version.split('.')[0..-1].join('.') + Gem::Version.new(version) <= Gem::Version.new(max_version) && + Gem::Version.new(version) >= Gem::Version.new(min_version) + end + + def matching_version(version) + uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml' + versions = Net::HTTP.get_response(URI.parse(uri)).body + versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) + versions.map! { |e| e.first.split('-neo4j-') } + versions.select { |e| match_version?(version, e.last) }.last + end + + desc 'install neo4j_spatial into /db/neo4j/[env]/plugins' + task :install, :environment do |_, args| + args.with_defaults(environment: 'development') + puts "Install Neo4j Spatial (#{args[:environment]} environment)..." + + url = 'https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial' + input_version = ENV['NEO4J_VERSION'] + fail ArgumentError, 'Missing NEO4J_VERSION' unless input_version + spatial_version, neo4j_version = *matching_version(input_version) + + install_path = "db/neo4j/#{args[:environment]}/plugins" + + if neo4j_version[0].to_i < 3 + file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.zip" + system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true") + system_or_fail("unzip #{file_name} -d #{install_path}") + else + file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.jar" + system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true") + system_or_fail("mv #{file_name} #{install_path}") + end + end +end From 8e4632c6b35bb8a9ce1f47292539aec8875a4dfc Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sat, 4 Feb 2017 12:26:43 -0800 Subject: [PATCH 02/12] Make versions more flexible --- lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake index 13a6323..e42e483 100644 --- a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -6,7 +6,7 @@ end namespace :neo4j_spatial do def match_version?(version, max_version) - min_version = max_version.split('.')[0..-1].join('.') + min_version = max_version.split('.')[0..-2].join('.') Gem::Version.new(version) <= Gem::Version.new(max_version) && Gem::Version.new(version) >= Gem::Version.new(min_version) end From 47b77bda02a3af5b1f24e712979776ec99695e21 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sat, 4 Feb 2017 12:39:01 -0800 Subject: [PATCH 03/12] Make version failure more helpful --- .../rake_tasks/neo4j_spatial.rake | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake index e42e483..599602a 100644 --- a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -11,12 +11,31 @@ namespace :neo4j_spatial do Gem::Version.new(version) >= Gem::Version.new(min_version) end + def fail_with_help(version, latest_versions) + message = <<-MSG + + No compatible version of neo4j_spatial was found for neo4j version #{version}. + The latest version is (neo4j_spatial=#{latest_versions[0]}, neo4j=#{latest_versions[1]}). + + To install neo4j_spatial for a different version, run: + NEO4J_VERSION='#{latest_versions[1]}' bundle exec rake neo4j_spatial:install + + MSG + + fail ArgumentError, message + end + def matching_version(version) uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml' versions = Net::HTTP.get_response(URI.parse(uri)).body versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) versions.map! { |e| e.first.split('-neo4j-') } versions.select { |e| match_version?(version, e.last) }.last + + compatible_versions = versions.select { |e| match_version?(version, e.last) }.last + fail_with_help(version, versions.last) if compatible_versions.nil? + + compatible_versions end desc 'install neo4j_spatial into /db/neo4j/[env]/plugins' From b8af182626b35a5ff4cf88c781d51455e0335bd9 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sat, 4 Feb 2017 12:47:59 -0800 Subject: [PATCH 04/12] Default to neo4j installed version so NEO4J_VERSION is not required --- lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake index 599602a..6b9244e 100644 --- a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -38,13 +38,18 @@ namespace :neo4j_spatial do compatible_versions end - desc 'install neo4j_spatial into /db/neo4j/[env]/plugins' + def neo4j_version_from_install(env) + server_file = Dir.glob("db/neo4j/#{env}/lib/neo4j-server-*.jar").first + server_file.match(/.*-server-(.*).jar$/)[1] if server_file + end + + desc 'Install neo4j_spatial into /db/neo4j/[env]/plugins' task :install, :environment do |_, args| args.with_defaults(environment: 'development') puts "Install Neo4j Spatial (#{args[:environment]} environment)..." url = 'https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial' - input_version = ENV['NEO4J_VERSION'] + input_version = ENV['NEO4J_VERSION'] || neo4j_version_from_install(args[:environment]) fail ArgumentError, 'Missing NEO4J_VERSION' unless input_version spatial_version, neo4j_version = *matching_version(input_version) From 1d8e0c36c0955efccc788383aa42e51806b22386 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sat, 4 Feb 2017 11:09:57 -0800 Subject: [PATCH 05/12] Update for newer version of neo4jrb and neo4jrb-core gems. + use spatial procedures instead of old REST posts + update gemfile for newer versions --- .gitignore | 10 ++ README.md | 26 +++++ Rakefile | 2 + lib/neo4j/active_node/spatial.rb | 41 ++++++- lib/neo4j/spatial.rb | 170 ++++++++++++++++++--------- neo4jrb_spatial.gemspec | 4 +- spec/neo4jrb_spatial_spec.rb | 189 ++++++++++++++----------------- spec/spec_helper.rb | 23 ++-- 8 files changed, 286 insertions(+), 179 deletions(-) diff --git a/.gitignore b/.gitignore index e6a6445..c15e9fa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,13 @@ .idea bin/ /db + +# shapfile stuff +*.CPG +*.dbf +*.prj +*.sbn +*.sbx +*.shp +*.shp.xml +*.shx diff --git a/README.md b/README.md index 34f0239..2a598a9 100644 --- a/README.md +++ b/README.md @@ -142,3 +142,29 @@ mostly works for an idea of the basics, just replace Neography-specific commands ## Contributions Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `rubocop` from the CLI. + +### Running Tests: + +Upon first running `bundle exec rake spec` or `bundle exec rspec spec`, you may run into an issue like this: + +``` +Failure/Error: Restaurant.delete_all + +Neo4j::DeprecatedSchemaDefinitionError: + Some schema elements were defined by the model (which is no longer supported), but they do not exist in the database. Run the following to create them: + + rake neo4j:generate_schema_migration[constraint,Restaurant,uuid] + + + And then run `rake neo4j:migrate` +... +``` + +To resolve, simply run the commands as it says (but maybe prefix `bundle exec` depending on your setup): + +``` +bundle exec rake neo4j:generate_schema_migration[constraint,Restaurant,uuid] +bundle exec rake neo4j:migrate +``` + +NOTE that if your NEO4J_URL is not the default, you will have to prefix while running migrate: `NEO4J_URL='http://localhost:7123' be rake neo4j:migrate` diff --git a/Rakefile b/Rakefile index 60c71c1..e3b7b8b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ require 'bundler/gem_tasks' +require 'neo4j' require 'neo4j/rake_tasks' require 'neo4jrb_spatial/rake_tasks' +load 'neo4j/tasks/migration.rake' task 'spec' do system_or_fail('rspec spec') diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index c3e8ea0..076dea6 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -8,7 +8,7 @@ def self.included(other) def add_to_spatial_index(index_name = nil) index = index_name || self.class.spatial_index_name fail 'index name not found' unless index - Neo4j::Session.current.add_node_to_spatial_index(index, self) + Neo4j::ActiveBase.current_session.add_node_to_spatial_index(index, self) end module ClassMethods @@ -28,15 +28,44 @@ def spatial_index(index_name = nil) end end + # TODO: here!!!! + # Fix the thing to parse the params_string (on the right) and make it + # work for one of the procedures (withinDistance, bbox, etc.) module Query class QueryProxy - def spatial_match(var, params_string, spatial_index = nil) + # def spatial_match_retro(var, params_string, spatial_index = nil) + # Neo4j::Session.current.query + # .start("#{var} = node:#{index}({spatial_params})") + # .proxy_as(model, var) + # .params(spatial_params: params_string) + # + # end + + def spatial_match(var, params, spatial_index = nil) index = model.spatial_index_name || spatial_index fail 'Cannot query without index. Set index in model or as third argument.' unless index - Neo4j::Session.current.query - .start("#{var} = node:#{index}({spatial_params})") - .proxy_as(model, var) - .params(spatial_params: params_string) + + if params.is_a?(String) + # TODO: deprecation warning + params = parse_retro_params(params) + end + + # spatial.bbox + # spatial.closest + # spatial.intersects + # spatial.withinDistance + end + + def parse_retro_params(params) + + end + + def within_distance(lat, lon, distance, layer_name = nil) + layer = model.spatial_index_name || layer_name + + Neo4j::ActiveBase.current_session + .within_distance(layer, lon, lat, distance, execute: false) + .proxy_as(model, :node) end end end diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index 738d21f..e8b4519 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -1,111 +1,171 @@ module Neo4j - module Server + module Core module Spatial def spatial? - Neo4j::Session.current.connection.get('/db/data/ext/SpatialPlugin').status == 200 + spatial_plugin + true + rescue Neo4j::Server::CypherResponse::ResponseError + false end def spatial_plugin - parse_response! Neo4j::Session.current.connection.get('/db/data/ext/SpatialPlugin').body + call_query = 'CALL spatial.procedures() YIELD name' + + query(call_query, {}).map(&:name) end - def add_point_layer(layer, lat = nil, lon = nil) + def add_point_layer(layer, lat = nil, lon = nil, execute: true) options = { layer: layer, - lat: lat || 'lat', - lon: lon || 'lon' + lon: lon || 'lon', + lat: lat || 'lat' } - spatial_post('/ext/SpatialPlugin/graphdb/addSimplePointLayer', options) + wrap_spatial_procedure('addPointLayerXY', options, execute) end - def add_editable_layer(layer, format = 'WKT', node_property_name = 'wkt') - options = { - layer: layer, - format: format, - nodePropertyName: node_property_name - } + def spatial_procedure(procedure_name, procedure_args) + call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') + call_query = "spatial.#{procedure_name}(#{call_params}) YIELD node" - spatial_post('/ext/SpatialPlugin/graphdb/addEditableLayer', options) + query_ = Query.new(session: self) + query_.call(call_query).params(procedure_args) end - def get_layer(layer) - options = { - layer: layer - } - spatial_post('/ext/SpatialPlugin/graphdb/getLayer', options) + def wrap_spatial_procedure(procedure_name, procedure_args, execute = true) + procedure = spatial_procedure(procedure_name, procedure_args) + procedure = execute_and_format_response(procedure) if execute + procedure + end + + def execute_and_format_response(procedure) + procedure.response.map(&:node) end - def add_geometry_to_layer(layer, geometry) + def add_editable_layer(layer, format = 'WKT', node_property_name = 'wkt', execute: true) + # UGH don't know how to handle non-WKT things. Is this necessary? Maybe... + # TODO: remove old version that uses spatial_post + if format == 'WKT' + options = { + layer: layer, + node_property_name: node_property_name + } + + wrap_spatial_procedure('addWKTLayer', options, execute) + else + options = { + layer: layer, + format: format, + nodePropertyName: node_property_name + } + + spatial_post('/ext/SpatialPlugin/graphdb/addEditableLayer', options) + end + end + + def get_layer(layer, execute: true) + options = {layer: layer} + wrap_spatial_procedure('layer', options, execute) + end + + def add_geometry_to_layer(layer, geometry, execute: true) options = { layer: layer, geometry: geometry } - spatial_post('/ext/SpatialPlugin/graphdb/addGeometryWKTToLayer', options) + wrap_spatial_procedure('addWKT', options, execute) end - def edit_geometry_from_layer(layer, geometry, node) + def edit_geometry_from_layer(layer, geometry, node, execute: true) options = { layer: layer, geometry: geometry, geometryNodeId: get_id(node) } - spatial_post('/ext/SpatialPlugin/graphdb/updateGeometryFromWKT', options) + wrap_spatial_procedure('updateFromWKT', options, execute) end - def add_node_to_layer(layer, node) + # Hmmm this one has trouble, because we actually need to MATCH the node itself... + # Wish this could be cleaner but for now it works... + def add_node_to_layer(layer, node, execute: true) options = { layer: layer, - node: "#{resource_url}node/#{node.neo_id}" + node_id: node.neo_id } - spatial_post('/ext/SpatialPlugin/graphdb/addNodeToLayer', options) + + query_ = Query.new(session: self) + procedure = query_.match(:n) + .where('id(n) = {node_id}') + .with(:n).call('spatial.addNode({layer}, n) YIELD node') + .return('node') + .params(options) + + procedure = execute_and_format_response(procedure) if execute + procedure end - def find_geometries_in_bbox(layer, minx, maxx, miny, maxy) + def find_geometries_in_bbox(layer, minx, maxx, miny, maxy, execute: true) options = { layer: layer, - minx: minx, - maxx: maxx, - miny: miny, - maxy: maxy + min: {lon: minx, lat: miny}, + max: {lon: maxx, lat: maxy} } - spatial_post('/ext/SpatialPlugin/graphdb/findGeometriesInBBox', options) + + wrap_spatial_procedure('bbox', options, execute) end - def find_geometries_within_distance(layer, pointx, pointy, distance) + def find_geometries_within_distance(layer, pointx, pointy, distance, execute: true) + warn_deprecated(name: __method__, preferred: 'within_distance') + within_distance(layer, pointx, pointy, distance, execute: execute) + end + + def within_distance(layer, pointx, pointy, distance, execute: true) options = { layer: layer, - pointX: pointx, - pointY: pointy, + coordinate: {lon: pointx, lat: pointy}, distanceInKm: distance } - spatial_post('/ext/SpatialPlugin/graphdb/findGeometriesWithinDistance', options) + + wrap_spatial_procedure('withinDistance', options, execute) end - def create_spatial_index(name, type = nil, lat = nil, lon = nil) + def add_layer(name, type = nil, lat = nil, lon = nil, execute: true) + # supported names for type are: 'SimplePoint', 'WKT', 'WKB' + type ||= 'SimplePoint' + + # Hmm should keep this or let it break? + type = 'SimplePoint' if type == 'point' + options = { name: name, - config: { - provider: 'spatial', - geometry_type: type || 'point', - lat: lat || 'lat', - lon: lon || 'lon' - } + type: type || 'point', + encoderConfig: "#{lon || 'lon'}:#{lat || 'lat'}" } - spatial_post('/index/node', options) + wrap_spatial_procedure('addLayer', options, execute) + end + + def create_spatial_index(name, type = nil, lat = nil, lon = nil) + warn_deprecated(name: __method__, preferred: 'add_layer') + add_layer(name, type, lat, lon) end def add_node_to_spatial_index(index, node) - options = { - uri: "/#{get_id(node)}", - key: 'k', - value: 'v' - } - spatial_post("/index/node/#{index}", options) + warn_deprecated(name: __method__, preferred: 'add_node_to_layer') + add_node_to_layer(index, node) + end + + def import_shapefile_to_layer(layer, file_uri) + options = {layer: layer, file_uri: file_uri} + + spatial_procedure('importShapefileToLayer', options) end private + def warn_deprecated(name:, preferred:) + puts "WARNING: method '#{name}' is deprecated. Please use #{preferred}, which does the same thing." + end + def spatial_post(path, options) parse_response! Neo4j::Session.current.connection.post("/db/data/#{path}", options).body end @@ -117,9 +177,9 @@ def parse_response!(response) def request_error!(code, message, stack_trace) fail Neo4jrbSpatial::RequestError, <<-ERROR - #{ANSI::CYAN}#{code}#{ANSI::CLEAR}: #{message} - #{stack_trace} -ERROR + #{ANSI::CYAN}#{code}#{ANSI::CLEAR}: #{message} + #{stack_trace} + ERROR end def get_id(id) @@ -128,7 +188,7 @@ def get_id(id) when Array get_id(id.first) when Hash - id[:self].split('/').last + id[:id] when String id.split('/').last else @@ -137,7 +197,7 @@ def get_id(id) end end - class CypherSession < Neo4j::Session + class CypherSession include Spatial end end diff --git a/neo4jrb_spatial.gemspec b/neo4jrb_spatial.gemspec index 043e556..e928742 100644 --- a/neo4jrb_spatial.gemspec +++ b/neo4jrb_spatial.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec' spec.add_development_dependency 'pry' - spec.add_dependency 'neo4j', '>= 5.0.1', '< 8' - spec.add_dependency 'neo4j-core', '>= 5.0.1', '< 7' + spec.add_dependency 'neo4j', '>= 8.0.6' + spec.add_dependency 'neo4j-core', '>= 7', '< 7.1.0' spec.add_dependency 'neo4j-rake_tasks', '~> 0.3' end diff --git a/spec/neo4jrb_spatial_spec.rb b/spec/neo4jrb_spatial_spec.rb index 57fb696..3e16b59 100644 --- a/spec/neo4jrb_spatial_spec.rb +++ b/spec/neo4jrb_spatial_spec.rb @@ -1,60 +1,81 @@ require 'spec_helper' -describe Neo4j::Server::Spatial do - let(:neo) { Neo4j::Session.current } +describe Neo4j::Core::Spatial do + let(:neo) { current_session } describe 'find the spatial plugin' do it 'can get a description of the spatial plugin' do - si = neo.spatial_plugin - expect(si).not_to be_nil - expect(si[:graphdb][:addEditableLayer]).not_to be_nil + expect(neo.spatial?).to eq(true) + + procedures = neo.spatial_plugin + expect(procedures).not_to be_nil + expect(procedures).to include('spatial.addWKTLayer') end end describe 'add a point layer' do it 'can add a simple point layer' do - pl = neo.add_point_layer('restaurants') - expect(pl).not_to be_nil - expect(pl.first[:data][:layer]).to eq('restaurants') - expect(pl.first[:data][:geomencoder_config]).to eq('lon:lat') + response = neo.add_point_layer('restaurants') + pl = response.first + + expect(pl.props[:layer]).to eq('restaurants') + expect(pl.props[:geomencoder_config]).to eq('lon:lat') end it 'can add a simple point layer with lat and long' do - pl = neo.add_point_layer('coffee_shops', 'latitude', 'longitude') - expect(pl).not_to be_nil - expect(pl.first[:data][:layer]).to eq('coffee_shops') - expect(pl.first[:data][:geomencoder_config]).to eq('longitude:latitude') + response = neo.add_point_layer('coffee_shops', 'latitude', 'longitude') + pl = response.first + + expect(pl.props[:layer]).to eq('coffee_shops') + expect(pl.props[:geomencoder_config]).to eq('longitude:latitude') end end describe 'add an editable layer' do it 'can add an editable layer' do - el = neo.add_editable_layer('zipcodes', 'WKT', 'wkt') + response = neo.add_editable_layer('zipcodes', 'WKT', 'wkt') + el = response.first expect(el).not_to be_nil - expect(el.first[:data][:layer]).to eq('zipcodes') - expect(el.first[:data][:geomencoder_config]).to eq('wkt') + expect(el.props[:layer]).to eq('zipcodes') + expect(el.props[:geomencoder_config]).to eq('wkt') end end describe 'get a spatial layer' do it 'can get a layer' do - sl = neo.get_layer('restaurants') + # TODO: should this just return one node instead of array? + sl = neo.get_layer('restaurants').first expect(sl).not_to be_nil - expect(sl.first[:data][:layer]).to eq('restaurants') + expect(sl.props[:layer]).to eq('restaurants') end end - describe 'create a spatial index' do - it 'can create a spatial index' do - index = neo.create_spatial_index('restaurants') - expect(index[:provider]).to eq('spatial') - expect(index[:geometry_type]).to eq('point') - expect(index[:lat]).to eq('lat') - expect(index[:lon]).to eq('lon') + describe 'add_layer' do + it 'works when passed WKT type' do + layer = neo.add_layer('testaurants', 'WKT').first + + expect(layer.props[:layer]).to eq('testaurants') + expect(layer.props[:layer_class]).to match('EditableLayerImpl') + expect(layer.props[:geomencoder]).to match('WKTGeometryEncoder') + expect(layer.props[:geomencoder_config]).to eq('lon:lat') end - it 'fails when passing an invalid name' do - expect { neo.create_spatial_index('') }.to raise_error Neo4jrbSpatial::RequestError, /not be empty/ + it 'works when passed WKB type' do + layer = neo.add_layer('bestaurants', 'WKB').first + + expect(layer.props[:layer]).to eq('bestaurants') + expect(layer.props[:layer_class]).to match('EditableLayerImpl') + expect(layer.props[:geomencoder]).to match('WKBGeometryEncoder') + expect(layer.props[:geomencoder_config]).to eq('lon:lat') + end + end + + describe 'create a spatial index' do + it 'can create a spatial index' do + layer = neo.create_spatial_index('layer_pretending_to_be_index').first + expect(layer.props[:geomencoder_config]).to eq('lon:lat') + expect(layer.props[:layer_class]).to match('SimplePointLayer') + expect(layer.props[:layer]).to eq('layer_pretending_to_be_index') end end @@ -63,7 +84,7 @@ geometry = 'LINESTRING (15.2 60.1, 15.3 60.1)' geo = neo.add_geometry_to_layer('zipcodes', geometry) expect(geo).not_to be_nil - expect(geo.first[:data][:wkt]).to eq(geometry) + expect(geo.first.props[:wkt]).to eq(geometry) end end @@ -72,26 +93,24 @@ geometry = 'LINESTRING (15.2 60.1, 15.3 60.1)' geo = neo.add_geometry_to_layer('zipcodes', geometry) expect(geo).not_to be_nil - expect(geo.first[:data][:wkt]).to eq(geometry) + expect(geo.first.props[:wkt]).to eq(geometry) geometry = 'LINESTRING (14.7 60.1, 15.3 60.1)' existing_geo = neo.edit_geometry_from_layer('zipcodes', geometry, geo) - expect(existing_geo.first[:data][:wkt]).to eq(geometry) - expect(existing_geo.first[:self].split('/').last.to_i).to eq(geo.first[:self].split('/').last.to_i) + expect(existing_geo.first.props[:wkt]).to eq(geometry) + expect(existing_geo.first.neo_id.to_i).to eq(geo.first.neo_id.to_i) end end describe 'add a node to a layer' do it 'can add a node to a simple point layer' do properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} - node = Neo4j::Node.create(properties, :Restaurant) + node_query = Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: properties}).return(:n) + node = neo.query(node_query).first.n + expect(node).not_to be_nil added = neo.add_node_to_layer('restaurants', node) - expect(added.first[:data][:lat]).to eq(properties[:lat]) - expect(added.first[:data][:lon]).to eq(properties[:lon]) - - added = neo.add_node_to_spatial_index('restaurants', node) - expect(added[:data][:lat]).to eq(properties[:lat]) - expect(added[:data][:lon]).to eq(properties[:lon]) + expect(added.first.props[:lat]).to eq(properties[:lat]) + expect(added.first.props[:lon]).to eq(properties[:lon]) end end @@ -100,35 +119,9 @@ properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} nodes = neo.find_geometries_in_bbox('restaurants', 87.5, 87.7, 41.7, 41.9) expect(nodes).not_to be_empty - result = nodes.find { |node| node[:data][:name] == "Max's Restaurant" } - expect(result[:data][:lat]).to eq(properties[:lat]) - expect(result[:data][:lon]).to eq(properties[:lon]) - end - - it 'can find a geometry in a bounding box using cypher' do - properties = {lat: 60.1, lon: 15.2} - neo.create_spatial_index('geombbcypher', 'point', 'lat', 'lon') - node = Neo4j::Node.create(properties, :dummy) - neo.add_node_to_spatial_index('geombbcypher', node) - existing_node = neo.query.start("node = node:geombbcypher('bbox:[15.0,15.3,60.0,60.2]')").pluck(:node).first - expect(existing_node).not_to be_nil - expect(existing_node.props[:lat]).to eq(properties[:lat]) - expect(existing_node.props[:lon]).to eq(properties[:lon]) - end - - it 'can find a geometry in a bounding box using cypher two' do - properties = {lat: 60.1, lon: 15.2} - neo.create_spatial_index('geombbcypher2', 'point', 'lat', 'lon') - node = Neo4j::Node.create(properties) - neo.add_node_to_spatial_index('geombbcypher2', node) - existing_node = node.query.start("node = node:geombbcypher2('bbox:[15.0,15.3,60.0,60.2]')").pluck(:node).first - expect(existing_node).not_to be_nil - expect(existing_node.props[:lat]).to eq(properties[:lat]) - expect(existing_node.props[:lon]).to eq(properties[:lon]) - end - - it 'fails when passing an invalid node' do - expect { neo.add_node_to_spatial_index('geombbcypher', nil) }.to raise_error Neo4jrbSpatial::RequestError, /For input string/ + result = nodes.find { |node| node.props[:name] == "Max's Restaurant" } + expect(result.props[:lat]).to eq(properties[:lat]) + expect(result.props[:lon]).to eq(properties[:lon]) end end @@ -137,52 +130,44 @@ properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} nodes = neo.find_geometries_within_distance('restaurants', 87.627, 41.881, 10) expect(nodes).not_to be_empty - result = nodes.find { |node| node[:data][:name] == "Max's Restaurant" } - expect(result[:data][:lat]).to eq(properties[:lat]) - expect(result[:data][:lon]).to eq(properties[:lon]) - end - - it 'can find a geometry within distance using cypher' do - properties = {lat: 60.1, lon: 15.2} - neo.create_spatial_index('geowdcypher', 'point', 'lat', 'lon') - node = Neo4j::Node.create(properties) - neo.add_node_to_spatial_index('geowdcypher', node) - existing_node = neo.query.start('n = node:geowdcypher({bbox})').params(bbox: 'withinDistance:[60.0,15.0,100.0]').pluck(:n).first - expect(existing_node).not_to be_nil - expect(existing_node.props[:lat]).to eq(properties[:lat]) - expect(existing_node.props[:lon]).to eq(properties[:lon]) - end - - it 'can find a geometry within distance using cypher 2' do - properties = {lat: 60.1, lon: 15.2} - neo.create_spatial_index('geowdcypher2', 'point', 'lat', 'lon') - node = Neo4j::Node.create(properties) - neo.add_node_to_spatial_index('geowdcypher2', node) - existing_node = neo.query.start('n = node:geowdcypher2({bbox})').params(bbox: 'withinDistance:[60.0,15.0,100.0]').pluck(:n).first - expect(existing_node).not_to be_nil - expect(existing_node.props[:lat]).to eq(properties[:lat]) - expect(existing_node.props[:lon]).to eq(properties[:lon]) + result = nodes.find { |node| node.props[:name] == "Max's Restaurant" } + expect(result.props[:lat]).to eq(properties[:lat]) + expect(result.props[:lon]).to eq(properties[:lon]) end end + # TODO: find small shapefile to do this with. + # describe 'importing shapefile' do + # it 'works' do + # layer = neo.add_editable_layer('cities', 'WKT', 'wkt') + # filepath = "#{File.dirname(__FILE__)}/../cities/Cities2015.shp" + # + # abc = neo.import_shapefile_to_layer('cities', filepath) + # binding.pry + # end + # end + describe 'ActiveNode integration' do let(:node) { Restaurant.create(name: "Chris's Restauarant", lat: 60.1, lon: 15.2) } let(:outside_node) { Restaurant.create(name: 'Lily Thai', lat: 59.0, lon: 14.9) } - before do - stub_const('Restaurant', Class.new do - include Neo4j::ActiveNode - include Neo4j::ActiveNode::Spatial - spatial_index 'restaurants' - property :name - property :lat - property :lon - end) + class Restaurant + include Neo4j::ActiveNode + include Neo4j::ActiveNode::Spatial + + spatial_index 'restaurants' + property :name + property :lat + property :lon + end + + before do Restaurant.delete_all [node, outside_node].each(&:add_to_spatial_index) end - let(:match) { Restaurant.all.spatial_match(:r, 'withinDistance:[60.0,15.0,100.0]') } + # let(:match) { Restaurant.all.spatial_match(:r, 'withinDistance:[60.0,15.0,100.0]') } + let(:match) { Restaurant.all.within_distance(60.0, 15.0, 100.0) } it 'is a QueryProxy' do expect(match).to respond_to(:to_cypher) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ed91921..4076561 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,37 +1,32 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'rspec' +require 'pry' require 'json' require 'neo4j-core' require 'neo4j' require 'neo4jrb_spatial' +require 'neo4j/core/cypher_session/adaptors/http' def server_url ENV['NEO4J_URL'] || 'http://localhost:7474' end -def create_server_session - Neo4j::Session.open(:server_db, server_url) -end - -def clear_model_memory_caches - Neo4j::ActiveRel::Types::WRAPPED_CLASSES.clear - Neo4j::ActiveNode::Labels::WRAPPED_CLASSES.clear - Neo4j::ActiveNode::Labels.clear_wrapped_models +def current_session + @current_session ||= begin + neo4j_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new(server_url) + session = Neo4j::Core::CypherSession.new(neo4j_adaptor) + Neo4j::ActiveBase.current_session = session + end end RSpec.configure do |c| c.before(:suite) do - create_server_session - Neo4j::Session.current.query('MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') + current_session.query('MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') end c.before do - curr_session = Neo4j::Session.current - curr_session.close if curr_session && !curr_session.is_a?(Neo4j::Server::CypherSession) - Neo4j::Session.current || create_server_session end c.after(:each) do - clear_model_memory_caches end end From a3dab0dafc6f1a5a4fd8828845b55b6ef4de3686 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Mon, 15 May 2017 18:32:58 -0700 Subject: [PATCH 06/12] Match new procedure calls --- lib/neo4j/spatial.rb | 182 ++++++++++++------------- spec/neo4jrb_spatial_spec.rb | 248 ++++++++++++++++++++--------------- 2 files changed, 234 insertions(+), 196 deletions(-) diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index e8b4519..b06fe59 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -2,87 +2,70 @@ module Neo4j module Core module Spatial def spatial? - spatial_plugin + spatial_procedures true - rescue Neo4j::Server::CypherResponse::ResponseError + rescue Neo4j::Core::CypherSession::CypherError false end - def spatial_plugin + def spatial_procedures call_query = 'CALL spatial.procedures() YIELD name' query(call_query, {}).map(&:name) end - def add_point_layer(layer, lat = nil, lon = nil, execute: true) + def add_layer(name, type = nil, lat = nil, lon = nil) + # supported names for type are: 'SimplePoint', 'WKT', 'WKB' + type ||= 'SimplePoint' + options = { - layer: layer, - lon: lon || 'lon', - lat: lat || 'lat' + name: name, + type: type || 'point', + encoderConfig: "#{lon || 'lon'}:#{lat || 'lat'}" } - - wrap_spatial_procedure('addPointLayerXY', options, execute) + wrap_spatial_procedure('addLayer', options) end - def spatial_procedure(procedure_name, procedure_args) - call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') - call_query = "spatial.#{procedure_name}(#{call_params}) YIELD node" - - query_ = Query.new(session: self) - query_.call(call_query).params(procedure_args) + def remove_layer(name) + options = { name: name } + wrap_spatial_procedure('removeLayer', options, node: false) end - def wrap_spatial_procedure(procedure_name, procedure_args, execute = true) - procedure = spatial_procedure(procedure_name, procedure_args) - procedure = execute_and_format_response(procedure) if execute - procedure - end + def add_point_layer(layer) + options = { layer: layer } - def execute_and_format_response(procedure) - procedure.response.map(&:node) + wrap_spatial_procedure('addPointLayer', options) end - def add_editable_layer(layer, format = 'WKT', node_property_name = 'wkt', execute: true) - # UGH don't know how to handle non-WKT things. Is this necessary? Maybe... - # TODO: remove old version that uses spatial_post - if format == 'WKT' - options = { - layer: layer, - node_property_name: node_property_name - } - - wrap_spatial_procedure('addWKTLayer', options, execute) - else - options = { - layer: layer, - format: format, - nodePropertyName: node_property_name - } + def add_wkt_layer(layer, node_property_name = 'wkt') + options = { + layer: layer, + node_property_name: node_property_name + } - spatial_post('/ext/SpatialPlugin/graphdb/addEditableLayer', options) - end + wrap_spatial_procedure('addWKTLayer', options) end def get_layer(layer, execute: true) options = {layer: layer} - wrap_spatial_procedure('layer', options, execute) + wrap_spatial_procedure('layer', options, execute: execute) end - def add_geometry_to_layer(layer, geometry, execute: true) + def add_wkt(layer, geometry, execute: true) options = { layer: layer, geometry: geometry } - wrap_spatial_procedure('addWKT', options, execute) + wrap_spatial_procedure('addWKT', options, execute: execute) end - def edit_geometry_from_layer(layer, geometry, node, execute: true) + def update_from_wkt(layer, geometry, node, execute: true) options = { layer: layer, geometry: geometry, geometryNodeId: get_id(node) } - wrap_spatial_procedure('updateFromWKT', options, execute) + wrap_spatial_procedure('updateFromWKT', options, execute: execute) end # Hmmm this one has trouble, because we actually need to MATCH the node itself... @@ -95,64 +78,58 @@ def add_node_to_layer(layer, node, execute: true) query_ = Query.new(session: self) procedure = query_.match(:n) - .where('id(n) = {node_id}') - .with(:n).call('spatial.addNode({layer}, n) YIELD node') - .return('node') - .params(options) + .where('id(n) = {node_id}') + .with(:n).call('spatial.addNode({layer}, n) YIELD node') + .return('node') + .params(options) procedure = execute_and_format_response(procedure) if execute procedure end - def find_geometries_in_bbox(layer, minx, maxx, miny, maxy, execute: true) - options = { - layer: layer, - min: {lon: minx, lat: miny}, - max: {lon: maxx, lat: maxy} - } - - wrap_spatial_procedure('bbox', options, execute) - end + def bbox(layer, min, max, execute: true) + options = { layer: layer, min: min, max: max } - def find_geometries_within_distance(layer, pointx, pointy, distance, execute: true) - warn_deprecated(name: __method__, preferred: 'within_distance') - within_distance(layer, pointx, pointy, distance, execute: execute) + wrap_spatial_procedure('bbox', options, execute: execute) end + alias find_geometries_in_bbox bbox - def within_distance(layer, pointx, pointy, distance, execute: true) + def within_distance(layer, coordinate, distance, execute: true) options = { layer: layer, - coordinate: {lon: pointx, lat: pointy}, + coordinate: coordinate, distanceInKm: distance } - wrap_spatial_procedure('withinDistance', options, execute) + wrap_spatial_procedure('withinDistance', options, execute: execute) end + alias find_geometries_within_distance within_distance - def add_layer(name, type = nil, lat = nil, lon = nil, execute: true) - # supported names for type are: 'SimplePoint', 'WKT', 'WKB' - type ||= 'SimplePoint' + def intersects(layer, geometry, execute: true) + options = { layer: layer, geometry: geometry } - # Hmm should keep this or let it break? - type = 'SimplePoint' if type == 'point' + wrap_spatial_procedure('intersects', options, execute: execute) + end + def closest(layer, coordinate, distance = 100, execute: true) options = { - name: name, - type: type || 'point', - encoderConfig: "#{lon || 'lon'}:#{lat || 'lat'}" + layer: layer, + coordinate: coordinate, + distanceInKm: distance } - wrap_spatial_procedure('addLayer', options, execute) - end - def create_spatial_index(name, type = nil, lat = nil, lon = nil) - warn_deprecated(name: __method__, preferred: 'add_layer') - add_layer(name, type, lat, lon) + wrap_spatial_procedure('closest', options, execute: execute) end - def add_node_to_spatial_index(index, node) - warn_deprecated(name: __method__, preferred: 'add_node_to_layer') - add_node_to_layer(index, node) - end + # def create_spatial_index(name, type = nil, lat = nil, lon = nil) + # warn_deprecated(name: __method__, preferred: 'add_layer') + # add_layer(name, type, lat, lon) + # end + # + # def add_node_to_spatial_index(index, node) + # warn_deprecated(name: __method__, preferred: 'add_node_to_layer') + # add_node_to_layer(index, node) + # end def import_shapefile_to_layer(layer, file_uri) options = {layer: layer, file_uri: file_uri} @@ -160,21 +137,48 @@ def import_shapefile_to_layer(layer, file_uri) spatial_procedure('importShapefileToLayer', options) end - private + protected - def warn_deprecated(name:, preferred:) - puts "WARNING: method '#{name}' is deprecated. Please use #{preferred}, which does the same thing." + def spatial_procedure(procedure_name, procedure_args) + call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') + call_query = "spatial.#{procedure_name}(#{call_params}) YIELD node" + + query_ = Query.new(session: self) + query_.call(call_query).params(procedure_args) + end + + def spatial_procedure_without_node(procedure_name, procedure_args) + call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') + call_query = "spatial.#{procedure_name}(#{call_params})" + + query_ = Query.new(session: self) + query_.call(call_query).params(procedure_args) end - def spatial_post(path, options) - parse_response! Neo4j::Session.current.connection.post("/db/data/#{path}", options).body + def wrap_spatial_procedure(procedure_name, procedure_args, execution_args = {}) + execute = execution_args.fetch(:execute, true) + node = execution_args.fetch(:node, true) + + procedure = if node + spatial_procedure(procedure_name, procedure_args) + else + spatial_procedure_without_node(procedure_name, procedure_args) + end + + procedure = execute_and_format_response(procedure) if execute + procedure end - def parse_response!(response) - request_error!(response[:exception], response[:message], response[:stack_trace]) if response.is_a?(Hash) && response[:exception] - response + def execute_and_format_response(procedure) + procedure.response.map do |res| + res.respond_to?(:node) ? res.node : res + end end + # def warn_deprecated(name:, preferred:) + # puts "WARNING: method '#{name}' is deprecated. Please use #{preferred}, which does the same thing." + # end + def request_error!(code, message, stack_trace) fail Neo4jrbSpatial::RequestError, <<-ERROR #{ANSI::CYAN}#{code}#{ANSI::CLEAR}: #{message} diff --git a/spec/neo4jrb_spatial_spec.rb b/spec/neo4jrb_spatial_spec.rb index 3e16b59..faef51b 100644 --- a/spec/neo4jrb_spatial_spec.rb +++ b/spec/neo4jrb_spatial_spec.rb @@ -4,105 +4,130 @@ let(:neo) { current_session } describe 'find the spatial plugin' do - it 'can get a description of the spatial plugin' do + it 'can get a list of the spatial plugin procedures' do expect(neo.spatial?).to eq(true) - procedures = neo.spatial_plugin + procedures = neo.spatial_procedures expect(procedures).not_to be_nil expect(procedures).to include('spatial.addWKTLayer') end end - describe 'add a point layer' do - it 'can add a simple point layer' do - response = neo.add_point_layer('restaurants') - pl = response.first + describe 'adding a layer (and removing)' do + describe '#add_layer' do + it 'works when passed SimplePoint type' do + layer = neo.add_layer('simple_layer', 'SimplePoint').first - expect(pl.props[:layer]).to eq('restaurants') - expect(pl.props[:geomencoder_config]).to eq('lon:lat') - end + expect(layer.props[:layer]).to eq('simple_layer') + expect(layer.props[:layer_class]).to match('SimplePointLayer') + expect(layer.props[:geomencoder]).to match('SimplePointEncoder') + expect(layer.props[:geomencoder_config]).to eq('lon:lat') - it 'can add a simple point layer with lat and long' do - response = neo.add_point_layer('coffee_shops', 'latitude', 'longitude') - pl = response.first + neo.remove_layer('simple_layer') + end - expect(pl.props[:layer]).to eq('coffee_shops') - expect(pl.props[:geomencoder_config]).to eq('longitude:latitude') - end - end + it 'works when passed different lat and lon configs' do + layer = neo.add_layer('simple_layer_config', 'SimplePoint', 'attitude', 'fortitude').first - describe 'add an editable layer' do - it 'can add an editable layer' do - response = neo.add_editable_layer('zipcodes', 'WKT', 'wkt') - el = response.first - expect(el).not_to be_nil - expect(el.props[:layer]).to eq('zipcodes') - expect(el.props[:geomencoder_config]).to eq('wkt') - end - end + expect(layer.props[:layer]).to eq('simple_layer_config') + expect(layer.props[:layer_class]).to match('SimplePointLayer') + expect(layer.props[:geomencoder]).to match('SimplePointEncoder') + expect(layer.props[:geomencoder_config]).to eq('fortitude:attitude') - describe 'get a spatial layer' do - it 'can get a layer' do - # TODO: should this just return one node instead of array? - sl = neo.get_layer('restaurants').first - expect(sl).not_to be_nil - expect(sl.props[:layer]).to eq('restaurants') + neo.remove_layer('simple_layer_config') + end + + it 'works when passed WKT type' do + layer = neo.add_layer('wkt_layer', 'WKT').first + + expect(layer.props[:layer]).to eq('wkt_layer') + expect(layer.props[:layer_class]).to match('EditableLayerImpl') + expect(layer.props[:geomencoder]).to match('WKTGeometryEncoder') + expect(layer.props[:geomencoder_config]).to eq('lon:lat') + + neo.remove_layer('wkt_layer') + end + + it 'works when passed WKB type' do + layer = neo.add_layer('wkb_layer', 'WKB').first + + expect(layer.props[:layer]).to eq('wkb_layer') + expect(layer.props[:layer_class]).to match('EditableLayerImpl') + expect(layer.props[:geomencoder]).to match('WKBGeometryEncoder') + expect(layer.props[:geomencoder_config]).to eq('lon:lat') + + neo.remove_layer('wkb_layer') + end end - end - describe 'add_layer' do - it 'works when passed WKT type' do - layer = neo.add_layer('testaurants', 'WKT').first + describe '#add_point_layer' do + it 'can add a simple point layer' do + response = neo.add_point_layer('restaurants').first - expect(layer.props[:layer]).to eq('testaurants') - expect(layer.props[:layer_class]).to match('EditableLayerImpl') - expect(layer.props[:geomencoder]).to match('WKTGeometryEncoder') - expect(layer.props[:geomencoder_config]).to eq('lon:lat') + expect(response.props[:layer]).to eq('restaurants') + neo.remove_layer('restaurants') + end end - it 'works when passed WKB type' do - layer = neo.add_layer('bestaurants', 'WKB').first + describe '#add_wkt_layer' do + it 'can add a wkt layer' do + response = neo.add_wkt_layer('zipcodes', 'zone_area') + el = response.first + + expect(el).not_to be_nil + expect(el.props[:layer]).to eq('zipcodes') + expect(el.props[:geomencoder_config]).to eq('zone_area') - expect(layer.props[:layer]).to eq('bestaurants') - expect(layer.props[:layer_class]).to match('EditableLayerImpl') - expect(layer.props[:geomencoder]).to match('WKBGeometryEncoder') - expect(layer.props[:geomencoder_config]).to eq('lon:lat') + neo.remove_layer('zipcodes') + end end end - describe 'create a spatial index' do - it 'can create a spatial index' do - layer = neo.create_spatial_index('layer_pretending_to_be_index').first - expect(layer.props[:geomencoder_config]).to eq('lon:lat') - expect(layer.props[:layer_class]).to match('SimplePointLayer') - expect(layer.props[:layer]).to eq('layer_pretending_to_be_index') + + describe 'get a spatial layer' do + it 'can get a layer' do + neo.add_point_layer('restaurants') + + layer = neo.get_layer('restaurants').first + expect(layer).not_to be_nil + + expect(layer.props[:layer]).to eq('restaurants') + + neo.remove_layer('restaurants') end end describe 'add geometry to spatial layer' do it 'can add a geometry' do + neo.add_wkt_layer('zipcodes') + geometry = 'LINESTRING (15.2 60.1, 15.3 60.1)' - geo = neo.add_geometry_to_layer('zipcodes', geometry) + geo = neo.add_wkt('zipcodes', geometry) expect(geo).not_to be_nil expect(geo.first.props[:wkt]).to eq(geometry) + + neo.remove_layer('zipcodes') end end describe 'update geometry from spatial layer' do it 'can update a geometry' do + neo.add_wkt_layer('zipcodes') + geometry = 'LINESTRING (15.2 60.1, 15.3 60.1)' - geo = neo.add_geometry_to_layer('zipcodes', geometry) + geo = neo.add_wkt('zipcodes', geometry) expect(geo).not_to be_nil expect(geo.first.props[:wkt]).to eq(geometry) geometry = 'LINESTRING (14.7 60.1, 15.3 60.1)' - existing_geo = neo.edit_geometry_from_layer('zipcodes', geometry, geo) + existing_geo = neo.update_from_wkt('zipcodes', geometry, geo) expect(existing_geo.first.props[:wkt]).to eq(geometry) expect(existing_geo.first.neo_id.to_i).to eq(geo.first.neo_id.to_i) end end - describe 'add a node to a layer' do - it 'can add a node to a simple point layer' do + describe '#add_node_to_layer' do + it 'works' do + neo.add_layer('restaurants') properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} node_query = Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: properties}).return(:n) node = neo.query(node_query).first.n @@ -111,74 +136,83 @@ added = neo.add_node_to_layer('restaurants', node) expect(added.first.props[:lat]).to eq(properties[:lat]) expect(added.first.props[:lon]).to eq(properties[:lon]) - end - end - describe 'find geometries in a bounding box' do - it 'can find a geometry in a bounding box' do - properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} - nodes = neo.find_geometries_in_bbox('restaurants', 87.5, 87.7, 41.7, 41.9) - expect(nodes).not_to be_empty - result = nodes.find { |node| node.props[:name] == "Max's Restaurant" } - expect(result.props[:lat]).to eq(properties[:lat]) - expect(result.props[:lon]).to eq(properties[:lon]) + neo.remove_layer('restaurants') end end - describe 'find geometries within distance' do - it 'can find a geometry within distance' do - properties = {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} - nodes = neo.find_geometries_within_distance('restaurants', 87.627, 41.881, 10) - expect(nodes).not_to be_empty - result = nodes.find { |node| node.props[:name] == "Max's Restaurant" } - expect(result.props[:lat]).to eq(properties[:lat]) - expect(result.props[:lon]).to eq(properties[:lon]) + describe 'spatial matching queries' do + let(:properties) { { name: "Max's Restaurant", lat: 41.8819, lon: 87.6278 } } + let(:node_query) { Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: properties}).return(:n) } + + before do + neo.add_layer('restaurants') + node = neo.query(node_query).first.n + neo.add_node_to_layer('restaurants', node) end - end - # TODO: find small shapefile to do this with. - # describe 'importing shapefile' do - # it 'works' do - # layer = neo.add_editable_layer('cities', 'WKT', 'wkt') - # filepath = "#{File.dirname(__FILE__)}/../cities/Cities2015.shp" - # - # abc = neo.import_shapefile_to_layer('cities', filepath) - # binding.pry - # end - # end + after do + neo.remove_layer('restaurants') + end - describe 'ActiveNode integration' do - let(:node) { Restaurant.create(name: "Chris's Restauarant", lat: 60.1, lon: 15.2) } - let(:outside_node) { Restaurant.create(name: 'Lily Thai', lat: 59.0, lon: 14.9) } + describe '#bbox (#find_geometries_in_bbox)' do + it 'can find a geometry in a bounding box' do + min = { lon: 87.5, lat: 41.7 } + max = { lon: 87.7, lat: 41.9 } - class Restaurant - include Neo4j::ActiveNode - include Neo4j::ActiveNode::Spatial + nodes = neo.find_geometries_in_bbox('restaurants', min, max) + expect(nodes).not_to be_empty - spatial_index 'restaurants' - property :name - property :lat - property :lon + result = nodes.find { |n| n.props[:name] == "Max's Restaurant" } + expect(result.props[:lat]).to eq(properties[:lat]) + expect(result.props[:lon]).to eq(properties[:lon]) + end end - before do - Restaurant.delete_all - [node, outside_node].each(&:add_to_spatial_index) + describe '#within_distance (#find_geometries_within_distance)' do + it 'can find a geometry within distance' do + nodes = neo.find_geometries_within_distance('restaurants', { lon: 87.627, lat: 41.881 }, 10) + expect(nodes).not_to be_empty + + result = nodes.find { |n| n.props[:name] == "Max's Restaurant" } + expect(result.props[:lat]).to eq(properties[:lat]) + expect(result.props[:lon]).to eq(properties[:lon]) + end end - # let(:match) { Restaurant.all.spatial_match(:r, 'withinDistance:[60.0,15.0,100.0]') } - let(:match) { Restaurant.all.within_distance(60.0, 15.0, 100.0) } + describe '#intersects' do + it 'returns nodes that intersect the given geometry' do + # TODO: could make a ruby class for geometries + geom = 'POLYGON ((87.5 41.7, 87.5 41.9, 87.7 41.9, 87.7 41.7, 87.5 41.7))' + nodes = neo.intersects('restaurants', geom) + expect(nodes.count).to eq(1) - it 'is a QueryProxy' do - expect(match).to respond_to(:to_cypher) + expect(nodes.first.props[:name]).to eq("Max's Restaurant") + end end - it 'matches to the node in the spatial index' do - expect(match.first).to eq node - end + describe '#closest' do + it 'returns the closest node to the given coordinate' do + other_properties = { name: "Min's Restaurant", lat: 41.87, lon: 87.6 } + other_node_query = Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: other_properties}).return(:n) + neo.query(other_node_query).first.n - it 'only returns expected nodes' do - expect(match.to_a).not_to include(outside_node) + coordinate = { lat: 41.89, lon: 87.63 } + + closest = neo.closest('restaurants', coordinate).first + expect(closest.props[:name]).to eq(properties[:name]) + end end end + + # TODO: find small shapefile to do this with. + # describe 'importing shapefile' do + # it 'works' do + # layer = neo.add_editable_layer('cities', 'WKT', 'wkt') + # filepath = "#{File.dirname(__FILE__)}/../cities/Cities2015.shp" + # + # abc = neo.import_shapefile_to_layer('cities', filepath) + # binding.pry + # end + # end end From e1e38e10c8babd5fa50ee07c941dc921ce8cb02f Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Tue, 16 May 2017 16:53:29 -0700 Subject: [PATCH 07/12] Add ActiveNode methods and tests --- lib/neo4j/active_node/spatial.rb | 89 ++++++++++++++++++++++---------- lib/neo4j/spatial.rb | 11 +--- spec/active_node_spatial_spec.rb | 88 +++++++++++++++++++++++++++++++ spec/neo4jrb_spatial_spec.rb | 2 +- 4 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 spec/active_node_spatial_spec.rb diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index 076dea6..890b3ab 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -5,26 +5,42 @@ def self.included(other) other.extend(ClassMethods) end - def add_to_spatial_index(index_name = nil) - index = index_name || self.class.spatial_index_name - fail 'index name not found' unless index - Neo4j::ActiveBase.current_session.add_node_to_spatial_index(index, self) + def add_to_spatial_layer(layer_name = nil) + layer = layer_name || self.class.spatial_layer_name + fail 'layer name not found' unless layer + Neo4j::ActiveBase.current_session.add_node_to_layer(layer, self) end module ClassMethods - attr_reader :spatial_index_name - def spatial_index(index_name = nil) - return spatial_index_name unless index_name - # create_index_callback(index_name) - @spatial_index_name = index_name + attr_reader :spatial_layer_name + attr_reader :spatial_layer_type + attr_reader :spatial_layer_config + + def spatial_layer(layer_name = nil, options = {}) + return spatial_layer_name unless layer_name + + @spatial_layer_name = layer_name + @spatial_layer_type = options.fetch(:type, 'SimplePoint') + @spatial_layer_config = options.fetch(:config, 'lon:lat') + + spatial_layer_name end - # This will not work for now. Neo4j Spatial's REST API doesn't seem to work within transactions. - # def create_index_callback(index_name) - # after_create(proc { |node| Neo4j::Session.current.add_node_to_spatial_index(index_name, node) }) - # end + def create_layer + fail 'layer name not found' unless spatial_layer_name + + lon_name, lat_name = spatial_layer_config.split(':') + + Neo4j::ActiveBase.current_session.add_layer(spatial_layer_name, spatial_layer_type, lat_name, lon_name) + end - # private :create_index_callback + def remove_layer + fail 'layer name not found' unless spatial_layer_name + + Neo4j::ActiveBase.current_session.remove_layer(spatial_layer_name) + end + + # private end end @@ -33,38 +49,57 @@ def spatial_index(index_name = nil) # work for one of the procedures (withinDistance, bbox, etc.) module Query class QueryProxy - # def spatial_match_retro(var, params_string, spatial_index = nil) + # def spatial_match_retro(var, params_string, spatial_layer = nil) # Neo4j::Session.current.query - # .start("#{var} = node:#{index}({spatial_params})") + # .start("#{var} = node:#{layer}({spatial_params})") # .proxy_as(model, var) # .params(spatial_params: params_string) # # end - def spatial_match(var, params, spatial_index = nil) - index = model.spatial_index_name || spatial_index - fail 'Cannot query without index. Set index in model or as third argument.' unless index + # NOTE: should these be scopes? + def spatial_match(var, params, spatial_layer = nil) + layer = model.spatial_layer_name || spatial_layer + fail 'Cannot query without layer. Set layer in model or as third argument.' unless layer if params.is_a?(String) # TODO: deprecation warning params = parse_retro_params(params) end - - # spatial.bbox - # spatial.closest - # spatial.intersects - # spatial.withinDistance end def parse_retro_params(params) + end + + def within_distance(coordinate, distance, layer_name = nil) + layer = model.spatial_layer_name || layer_name + Neo4j::ActiveBase.current_session + .within_distance(layer, coordinate, distance, execute: false) + .proxy_as(model, :node) + end + + def bbox(min, max, layer_name = nil) + layer = model.spatial_layer_name || layer_name + + Neo4j::ActiveBase.current_session + .bbox(layer, min, max, execute: false) + .proxy_as(model, :node) + end + + def closest(coordinate, distance = 100, layer_name = nil) + layer = model.spatial_layer_name || layer_name + + Neo4j::ActiveBase.current_session + .closest(layer, coordinate, distance, execute: false) + .proxy_as(model, :node) end - def within_distance(lat, lon, distance, layer_name = nil) - layer = model.spatial_index_name || layer_name + def intersects(geometry, layer_name = nil) + layer = model.spatial_layer_name || layer_name Neo4j::ActiveBase.current_session - .within_distance(layer, lon, lat, distance, execute: false) + .intersects(layer, geometry, execute: false) .proxy_as(model, :node) end end diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index b06fe59..740d521 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -111,6 +111,7 @@ def intersects(layer, geometry, execute: true) wrap_spatial_procedure('intersects', options, execute: execute) end + # TODO: figure out what closest is supposed to do... def closest(layer, coordinate, distance = 100, execute: true) options = { layer: layer, @@ -121,16 +122,6 @@ def closest(layer, coordinate, distance = 100, execute: true) wrap_spatial_procedure('closest', options, execute: execute) end - # def create_spatial_index(name, type = nil, lat = nil, lon = nil) - # warn_deprecated(name: __method__, preferred: 'add_layer') - # add_layer(name, type, lat, lon) - # end - # - # def add_node_to_spatial_index(index, node) - # warn_deprecated(name: __method__, preferred: 'add_node_to_layer') - # add_node_to_layer(index, node) - # end - def import_shapefile_to_layer(layer, file_uri) options = {layer: layer, file_uri: file_uri} diff --git a/spec/active_node_spatial_spec.rb b/spec/active_node_spatial_spec.rb new file mode 100644 index 0000000..379300e --- /dev/null +++ b/spec/active_node_spatial_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Neo4j::ActiveNode::Spatial do + let(:neo) { current_session } + let(:node) { Restaurant.create(name: "Chris's Restauarant", lat: 60.1, lon: 15.2) } + let(:outside_node) { Restaurant.create(name: 'Lily Thai', lat: 59.0, lon: 14.9) } + + class Restaurant + include Neo4j::ActiveNode + include Neo4j::ActiveNode::Spatial + + spatial_layer 'restaurants' # , type: 'SimplePoint', config: 'lon:lat' + property :name + property :lat + property :lon + end + + before do + Restaurant.delete_all + Restaurant.create_layer + [node, outside_node].each(&:add_to_spatial_layer) + end + + after do + Restaurant.remove_layer + end + + # let(:match) { Restaurant.all.spatial_match(:r, 'withinDistance:[60.0,15.0,100.0]') } + describe '#within_distance' do + let(:match) { Restaurant.all.within_distance({lat: 60.0, lon: 15.0}, 100.0) } + + it 'is a QueryProxy' do + expect(match).to respond_to(:to_cypher) + end + + it 'returns nodes within the given distance of the point' do + expect(match.first).to eq node + end + + it 'only returns expected nodes' do + expect(match.to_a).not_to include(outside_node) + end + end + + describe '#bbox' do + let(:min) { {lat: 59.9, lon: 14.9} } + let(:max) { {lat: 60.2, lon: 15.3} } + let(:match) { Restaurant.all.bbox(min, max) } + + it 'returns nodes that are inside the given bbox' do + nodes = match.to_a + expect(nodes.count).to eq(1) + expect(nodes.first).to eq(node) + end + + it 'is chainable' do + new_match = match.within_distance({lat: 60.0, lon: 15.0}, 100.0) + + expect(new_match).to respond_to(:to_cypher) + expect(new_match.first).to eq(node) + end + end + + # describe '#closest' do + # # this point is closer to outside_node + # # let(:coordinate) { {lat: 59.1, lon: 15.0} } + # # let(:coordinate) { {lat: 60.0, lon: 15.1} } + # let(:match) { Restaurant.all.closest(coordinate) } + # + # it 'returns the closest node first' do + # puts match.to_a + # # expect(match.count).to eq(1) + # expect(match.first).to eq(outside_node) + # end + # end + + describe '#intersects' do + let(:geom) { 'POLYGON ((15.3 60.1, 15.3 58.9, 14.8 58.9, 14.8 60.1, 15.3 60.1))' } + let(:match) { Restaurant.all.intersects(geom) } + + it 'returns node that intersect the given geometry' do + expect(match.count).to eq(2) + + expect(match).to include(node) + expect(match).to include(outside_node) + end + end +end diff --git a/spec/neo4jrb_spatial_spec.rb b/spec/neo4jrb_spatial_spec.rb index faef51b..6cb374b 100644 --- a/spec/neo4jrb_spatial_spec.rb +++ b/spec/neo4jrb_spatial_spec.rb @@ -182,7 +182,6 @@ describe '#intersects' do it 'returns nodes that intersect the given geometry' do - # TODO: could make a ruby class for geometries geom = 'POLYGON ((87.5 41.7, 87.5 41.9, 87.7 41.9, 87.7 41.7, 87.5 41.7))' nodes = neo.intersects('restaurants', geom) expect(nodes.count).to eq(1) @@ -192,6 +191,7 @@ end describe '#closest' do + # TODO: poor test it 'returns the closest node to the given coordinate' do other_properties = { name: "Min's Restaurant", lat: 41.87, lon: 87.6 } other_node_query = Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: other_properties}).return(:n) From 726eb05bf05a9ae5864da88290790836ee129166 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Wed, 17 May 2017 14:28:10 -0700 Subject: [PATCH 08/12] update rubocop and clean unused code --- .rubocop.yml | 5 +- .travis.yml | 12 +--- lib/neo4j/active_node/spatial.rb | 27 --------- lib/neo4j/spatial.rb | 46 +++++---------- .../rake_tasks/neo4j_spatial.rake | 58 +++++++++---------- lib/neo4jrb_spatial/version.rb | 2 +- neo4jrb_spatial.gemspec | 2 +- spec/neo4jrb_spatial_spec.rb | 12 ++-- 8 files changed, 56 insertions(+), 108 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 2cfdd9f..1c7d8e1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,6 +21,9 @@ Style/SpaceInsideHashLiteralBraces: Style/FileName: Enabled: false +Metrics/AbcSize: + Max: 20 + #--------------------------- # Don't intend to fix these: #--------------------------- @@ -68,5 +71,3 @@ Style/Lambda: # Reason: I'm proud to be part of the double negative Ruby tradition Style/DoubleNegation: Enabled: false - - diff --git a/.travis.yml b/.travis.yml index 4078125..082fa20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ install: - "bundle exec rake neo4j_spatial:install" - "bundle exec rake neo4j:start --trace" - "sleep 20" + - "bundle exec rake neo4j:generate_schema_migration[constraint,Restaurant,uuid]" sudo: false cache: bundler script: @@ -15,14 +16,13 @@ script: language: ruby jdk: oraclejdk8 rvm: - - 2.0.0 - 2.2.4 - 2.3.1 - jruby-9.0.5.0 env: global: - JRUBY_OPTS="-J-Xmx1280m -Xcompile.invokedynamic=false -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-noverify -Xcompile.mode=OFF" - - NEO4J_VERSION=3.0.1 + - NEO4J_VERSION=3.1.0 matrix: - NEO4J_URL="http://localhost:7474" matrix: @@ -32,11 +32,3 @@ matrix: install: script: "bundle exec rubocop" env: "RUBOCOP=true" - - # Older versions of Neo4j with latest version of Ruby - - rvm: 2.3.1 - env: NEO4J_VERSION=2.3.3 - - rvm: 2.3.1 - env: NEO4J_VERSION=2.2.2 - - rvm: 2.3.1 - env: NEO4J_VERSION=2.1.8 diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index 890b3ab..b891113 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -39,38 +39,11 @@ def remove_layer Neo4j::ActiveBase.current_session.remove_layer(spatial_layer_name) end - - # private end end - # TODO: here!!!! - # Fix the thing to parse the params_string (on the right) and make it - # work for one of the procedures (withinDistance, bbox, etc.) module Query class QueryProxy - # def spatial_match_retro(var, params_string, spatial_layer = nil) - # Neo4j::Session.current.query - # .start("#{var} = node:#{layer}({spatial_params})") - # .proxy_as(model, var) - # .params(spatial_params: params_string) - # - # end - - # NOTE: should these be scopes? - def spatial_match(var, params, spatial_layer = nil) - layer = model.spatial_layer_name || spatial_layer - fail 'Cannot query without layer. Set layer in model or as third argument.' unless layer - - if params.is_a?(String) - # TODO: deprecation warning - params = parse_retro_params(params) - end - end - - def parse_retro_params(params) - end - def within_distance(coordinate, distance, layer_name = nil) layer = model.spatial_layer_name || layer_name diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index 740d521..5c0a53e 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -1,6 +1,8 @@ module Neo4j module Core + # rubocop:disable Metrics/ModuleLength module Spatial + # rubocop:enable Metrics/ModuleLength def spatial? spatial_procedures true @@ -27,12 +29,12 @@ def add_layer(name, type = nil, lat = nil, lon = nil) end def remove_layer(name) - options = { name: name } + options = {name: name} wrap_spatial_procedure('removeLayer', options, node: false) end def add_point_layer(layer) - options = { layer: layer } + options = {layer: layer} wrap_spatial_procedure('addPointLayer', options) end @@ -78,21 +80,21 @@ def add_node_to_layer(layer, node, execute: true) query_ = Query.new(session: self) procedure = query_.match(:n) - .where('id(n) = {node_id}') - .with(:n).call('spatial.addNode({layer}, n) YIELD node') - .return('node') - .params(options) + .where('id(n) = {node_id}') + .with(:n).call('spatial.addNode({layer}, n) YIELD node') + .return('node') + .params(options) procedure = execute_and_format_response(procedure) if execute procedure end def bbox(layer, min, max, execute: true) - options = { layer: layer, min: min, max: max } + options = {layer: layer, min: min, max: max} wrap_spatial_procedure('bbox', options, execute: execute) end - alias find_geometries_in_bbox bbox + alias_method :find_geometries_in_bbox, :bbox def within_distance(layer, coordinate, distance, execute: true) options = { @@ -103,10 +105,10 @@ def within_distance(layer, coordinate, distance, execute: true) wrap_spatial_procedure('withinDistance', options, execute: execute) end - alias find_geometries_within_distance within_distance + alias_method :find_geometries_within_distance, :within_distance def intersects(layer, geometry, execute: true) - options = { layer: layer, geometry: geometry } + options = {layer: layer, geometry: geometry} wrap_spatial_procedure('intersects', options, execute: execute) end @@ -166,29 +168,9 @@ def execute_and_format_response(procedure) end end - # def warn_deprecated(name:, preferred:) - # puts "WARNING: method '#{name}' is deprecated. Please use #{preferred}, which does the same thing." - # end - - def request_error!(code, message, stack_trace) - fail Neo4jrbSpatial::RequestError, <<-ERROR - #{ANSI::CYAN}#{code}#{ANSI::CLEAR}: #{message} - #{stack_trace} - ERROR - end - def get_id(id) - return id.neo_id if id.respond_to?(:neo_id) - case id - when Array - get_id(id.first) - when Hash - id[:id] - when String - id.split('/').last - else - id - end + return get_id(id.first) if id.is_a?(Array) + id.neo_id end end diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake index 6b9244e..1097681 100644 --- a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -4,45 +4,45 @@ def system_or_fail(command) system(command) || exit(1) end -namespace :neo4j_spatial do - def match_version?(version, max_version) - min_version = max_version.split('.')[0..-2].join('.') - Gem::Version.new(version) <= Gem::Version.new(max_version) && - Gem::Version.new(version) >= Gem::Version.new(min_version) - end +def match_version?(version, max_version) + min_version = max_version.split('.')[0..-2].join('.') + Gem::Version.new(version) <= Gem::Version.new(max_version) && + Gem::Version.new(version) >= Gem::Version.new(min_version) +end - def fail_with_help(version, latest_versions) - message = <<-MSG +def fail_with_help(version, latest_versions) + message = <<-MSG - No compatible version of neo4j_spatial was found for neo4j version #{version}. - The latest version is (neo4j_spatial=#{latest_versions[0]}, neo4j=#{latest_versions[1]}). + No compatible version of neo4j_spatial was found for neo4j version #{version}. + The latest version is (neo4j_spatial=#{latest_versions[0]}, neo4j=#{latest_versions[1]}). - To install neo4j_spatial for a different version, run: - NEO4J_VERSION='#{latest_versions[1]}' bundle exec rake neo4j_spatial:install + To install neo4j_spatial for a different version, run: + NEO4J_VERSION='#{latest_versions[1]}' bundle exec rake neo4j_spatial:install - MSG + MSG - fail ArgumentError, message - end + fail ArgumentError, message +end - def matching_version(version) - uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml' - versions = Net::HTTP.get_response(URI.parse(uri)).body - versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) - versions.map! { |e| e.first.split('-neo4j-') } - versions.select { |e| match_version?(version, e.last) }.last +def matching_version(version) + uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml' + versions = Net::HTTP.get_response(URI.parse(uri)).body + versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) + versions.map! { |e| e.first.split('-neo4j-') } + versions.select { |e| match_version?(version, e.last) }.last - compatible_versions = versions.select { |e| match_version?(version, e.last) }.last - fail_with_help(version, versions.last) if compatible_versions.nil? + compatible_versions = versions.select { |e| match_version?(version, e.last) }.last + fail_with_help(version, versions.last) if compatible_versions.nil? - compatible_versions - end + compatible_versions +end - def neo4j_version_from_install(env) - server_file = Dir.glob("db/neo4j/#{env}/lib/neo4j-server-*.jar").first - server_file.match(/.*-server-(.*).jar$/)[1] if server_file - end +def neo4j_version_from_install(env) + server_file = Dir.glob("db/neo4j/#{env}/lib/neo4j-server-*.jar").first + server_file.match(/.*-server-(.*).jar$/)[1] if server_file +end +namespace :neo4j_spatial do desc 'Install neo4j_spatial into /db/neo4j/[env]/plugins' task :install, :environment do |_, args| args.with_defaults(environment: 'development') diff --git a/lib/neo4jrb_spatial/version.rb b/lib/neo4jrb_spatial/version.rb index 2c62129..2275c35 100644 --- a/lib/neo4jrb_spatial/version.rb +++ b/lib/neo4jrb_spatial/version.rb @@ -1,3 +1,3 @@ module Neo4jrbSpatial - VERSION = '1.2.0' + VERSION = '1.2.0'.freeze end diff --git a/neo4jrb_spatial.gemspec b/neo4jrb_spatial.gemspec index e928742..17486fc 100644 --- a/neo4jrb_spatial.gemspec +++ b/neo4jrb_spatial.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rspec' spec.add_development_dependency 'pry' - spec.add_dependency 'neo4j', '>= 8.0.6' + spec.add_dependency 'neo4j', '>= 8.0.6', '<= 8.0.15' spec.add_dependency 'neo4j-core', '>= 7', '< 7.1.0' spec.add_dependency 'neo4j-rake_tasks', '~> 0.3' end diff --git a/spec/neo4jrb_spatial_spec.rb b/spec/neo4jrb_spatial_spec.rb index 6cb374b..5acfd4e 100644 --- a/spec/neo4jrb_spatial_spec.rb +++ b/spec/neo4jrb_spatial_spec.rb @@ -142,7 +142,7 @@ end describe 'spatial matching queries' do - let(:properties) { { name: "Max's Restaurant", lat: 41.8819, lon: 87.6278 } } + let(:properties) { {name: "Max's Restaurant", lat: 41.8819, lon: 87.6278} } let(:node_query) { Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: properties}).return(:n) } before do @@ -157,8 +157,8 @@ describe '#bbox (#find_geometries_in_bbox)' do it 'can find a geometry in a bounding box' do - min = { lon: 87.5, lat: 41.7 } - max = { lon: 87.7, lat: 41.9 } + min = {lon: 87.5, lat: 41.7} + max = {lon: 87.7, lat: 41.9} nodes = neo.find_geometries_in_bbox('restaurants', min, max) expect(nodes).not_to be_empty @@ -171,7 +171,7 @@ describe '#within_distance (#find_geometries_within_distance)' do it 'can find a geometry within distance' do - nodes = neo.find_geometries_within_distance('restaurants', { lon: 87.627, lat: 41.881 }, 10) + nodes = neo.find_geometries_within_distance('restaurants', {lon: 87.627, lat: 41.881}, 10) expect(nodes).not_to be_empty result = nodes.find { |n| n.props[:name] == "Max's Restaurant" } @@ -193,11 +193,11 @@ describe '#closest' do # TODO: poor test it 'returns the closest node to the given coordinate' do - other_properties = { name: "Min's Restaurant", lat: 41.87, lon: 87.6 } + other_properties = {name: "Min's Restaurant", lat: 41.87, lon: 87.6} other_node_query = Neo4j::Core::Query.new(session: neo).create(n: {Restaurant: other_properties}).return(:n) neo.query(other_node_query).first.n - coordinate = { lat: 41.89, lon: 87.63 } + coordinate = {lat: 41.89, lon: 87.63} closest = neo.closest('restaurants', coordinate).first expect(closest.props[:name]).to eq(properties[:name]) From 2fc2f5ded17b40be5d09b56317c4bd56807d234e Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sun, 28 May 2017 18:22:10 +0200 Subject: [PATCH 09/12] Address PR comments + remove rubocop exceptions + make constraint creation automatic + couple smaller refactors --- .rubocop.yml | 3 -- .travis.yml | 1 - README.md | 25 ++------------ lib/neo4j/active_node/spatial.rb | 34 ++++++++----------- lib/neo4j/spatial.rb | 30 +++------------- .../rake_tasks/neo4j_spatial.rake | 7 ++-- spec/spec_helper.rb | 5 +++ 7 files changed, 29 insertions(+), 76 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 1c7d8e1..1ce0ba3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -21,9 +21,6 @@ Style/SpaceInsideHashLiteralBraces: Style/FileName: Enabled: false -Metrics/AbcSize: - Max: 20 - #--------------------------- # Don't intend to fix these: #--------------------------- diff --git a/.travis.yml b/.travis.yml index 082fa20..4b00013 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ install: - "bundle exec rake neo4j_spatial:install" - "bundle exec rake neo4j:start --trace" - "sleep 20" - - "bundle exec rake neo4j:generate_schema_migration[constraint,Restaurant,uuid]" sudo: false cache: bundler script: diff --git a/README.md b/README.md index 2a598a9..da318c7 100644 --- a/README.md +++ b/README.md @@ -144,27 +144,6 @@ mostly works for an idea of the basics, just replace Neography-specific commands Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `rubocop` from the CLI. ### Running Tests: +`bundle exec rake spec` or `bundle exec rspec spec` -Upon first running `bundle exec rake spec` or `bundle exec rspec spec`, you may run into an issue like this: - -``` -Failure/Error: Restaurant.delete_all - -Neo4j::DeprecatedSchemaDefinitionError: - Some schema elements were defined by the model (which is no longer supported), but they do not exist in the database. Run the following to create them: - - rake neo4j:generate_schema_migration[constraint,Restaurant,uuid] - - - And then run `rake neo4j:migrate` -... -``` - -To resolve, simply run the commands as it says (but maybe prefix `bundle exec` depending on your setup): - -``` -bundle exec rake neo4j:generate_schema_migration[constraint,Restaurant,uuid] -bundle exec rake neo4j:migrate -``` - -NOTE that if your NEO4J_URL is not the default, you will have to prefix while running migrate: `NEO4J_URL='http://localhost:7123' be rake neo4j:migrate` +NOTE that if your NEO4J_URL is not the default, you will have to prefix while running migrate: `NEO4J_URL='http://localhost:7123' bundle exec rake spec` diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index b891113..e752308 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -6,38 +6,32 @@ def self.included(other) end def add_to_spatial_layer(layer_name = nil) - layer = layer_name || self.class.spatial_layer_name + layer = layer_name || self.class.spatial_layer.name fail 'layer name not found' unless layer Neo4j::ActiveBase.current_session.add_node_to_layer(layer, self) end module ClassMethods - attr_reader :spatial_layer_name - attr_reader :spatial_layer_type - attr_reader :spatial_layer_config + SpatialLayer = Struct.new(:name, :type, :config) - def spatial_layer(layer_name = nil, options = {}) - return spatial_layer_name unless layer_name - - @spatial_layer_name = layer_name - @spatial_layer_type = options.fetch(:type, 'SimplePoint') - @spatial_layer_config = options.fetch(:config, 'lon:lat') + attr_reader :spatial_layer - spatial_layer_name + def spatial_layer(layer_name = nil, options = {}) + @spatial_layer ||= SpatialLayer.new(layer_name, options.fetch(:type, 'SimplePoint'), options.fetch(:config, 'lon:lat')) end def create_layer - fail 'layer name not found' unless spatial_layer_name + fail 'layer not found' unless spatial_layer.name - lon_name, lat_name = spatial_layer_config.split(':') + lon_name, lat_name = spatial_layer.config.split(':') - Neo4j::ActiveBase.current_session.add_layer(spatial_layer_name, spatial_layer_type, lat_name, lon_name) + Neo4j::ActiveBase.current_session.add_layer(spatial_layer.name, spatial_layer.type, lat_name, lon_name) end def remove_layer - fail 'layer name not found' unless spatial_layer_name + fail 'layer not found' unless spatial_layer.name - Neo4j::ActiveBase.current_session.remove_layer(spatial_layer_name) + Neo4j::ActiveBase.current_session.remove_layer(spatial_layer.name) end end end @@ -45,7 +39,7 @@ def remove_layer module Query class QueryProxy def within_distance(coordinate, distance, layer_name = nil) - layer = model.spatial_layer_name || layer_name + layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session .within_distance(layer, coordinate, distance, execute: false) @@ -53,7 +47,7 @@ def within_distance(coordinate, distance, layer_name = nil) end def bbox(min, max, layer_name = nil) - layer = model.spatial_layer_name || layer_name + layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session .bbox(layer, min, max, execute: false) @@ -61,7 +55,7 @@ def bbox(min, max, layer_name = nil) end def closest(coordinate, distance = 100, layer_name = nil) - layer = model.spatial_layer_name || layer_name + layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session .closest(layer, coordinate, distance, execute: false) @@ -69,7 +63,7 @@ def closest(coordinate, distance = 100, layer_name = nil) end def intersects(geometry, layer_name = nil) - layer = model.spatial_layer_name || layer_name + layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session .intersects(layer, geometry, execute: false) diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index 5c0a53e..c6a844d 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -1,8 +1,6 @@ module Neo4j module Core - # rubocop:disable Metrics/ModuleLength module Spatial - # rubocop:enable Metrics/ModuleLength def spatial? spatial_procedures true @@ -11,9 +9,7 @@ def spatial? end def spatial_procedures - call_query = 'CALL spatial.procedures() YIELD name' - - query(call_query, {}).map(&:name) + query('CALL spatial.procedures() YIELD name').map(&:name) end def add_layer(name, type = nil, lat = nil, lon = nil) @@ -73,17 +69,12 @@ def update_from_wkt(layer, geometry, node, execute: true) # Hmmm this one has trouble, because we actually need to MATCH the node itself... # Wish this could be cleaner but for now it works... def add_node_to_layer(layer, node, execute: true) - options = { - layer: layer, - node_id: node.neo_id - } - query_ = Query.new(session: self) procedure = query_.match(:n) .where('id(n) = {node_id}') .with(:n).call('spatial.addNode({layer}, n) YIELD node') .return('node') - .params(options) + .params(layer: layer, node_id: node.neo_id) procedure = execute_and_format_response(procedure) if execute procedure @@ -132,17 +123,10 @@ def import_shapefile_to_layer(layer, file_uri) protected - def spatial_procedure(procedure_name, procedure_args) - call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') - call_query = "spatial.#{procedure_name}(#{call_params}) YIELD node" - - query_ = Query.new(session: self) - query_.call(call_query).params(procedure_args) - end - - def spatial_procedure_without_node(procedure_name, procedure_args) + def spatial_procedure(procedure_name, procedure_args, with_node = true) call_params = procedure_args.keys.map { |key| "{#{key}}" }.join(', ') call_query = "spatial.#{procedure_name}(#{call_params})" + call_query += ' YIELD node' if with_node query_ = Query.new(session: self) query_.call(call_query).params(procedure_args) @@ -152,11 +136,7 @@ def wrap_spatial_procedure(procedure_name, procedure_args, execution_args = {}) execute = execution_args.fetch(:execute, true) node = execution_args.fetch(:node, true) - procedure = if node - spatial_procedure(procedure_name, procedure_args) - else - spatial_procedure_without_node(procedure_name, procedure_args) - end + procedure = spatial_procedure(procedure_name, procedure_args, node) procedure = execute_and_format_response(procedure) if execute procedure diff --git a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake index 1097681..c5b867c 100644 --- a/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake +++ b/lib/neo4jrb_spatial/rake_tasks/neo4j_spatial.rake @@ -29,12 +29,11 @@ def matching_version(version) versions = Net::HTTP.get_response(URI.parse(uri)).body versions = versions.scan(/([a-z\-0-9\.]+)<\/version>/) versions.map! { |e| e.first.split('-neo4j-') } - versions.select { |e| match_version?(version, e.last) }.last - compatible_versions = versions.select { |e| match_version?(version, e.last) }.last - fail_with_help(version, versions.last) if compatible_versions.nil? + compatible_version = versions.select { |e| match_version?(version, e.last) }.last + fail_with_help(version, versions.last) if compatible_version.nil? - compatible_versions + compatible_version end def neo4j_version_from_install(env) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4076561..985aefb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,9 +19,14 @@ def current_session end end +def create_db_constraint + Neo4j::ActiveBase.label_object(:Restaurant).create_constraint(:uuid, type: :unique) +end + RSpec.configure do |c| c.before(:suite) do current_session.query('MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n, r') + create_db_constraint end c.before do From 1df8d2530653c61161eb694ab59a72074b9c7e1c Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Sun, 28 May 2017 18:46:30 +0200 Subject: [PATCH 10/12] Change ActiveNode methods to scopes --- lib/neo4j/active_node/spatial.rb | 57 +++++++++++++++----------------- spec/active_node_spatial_spec.rb | 8 ++--- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index e752308..fc22788 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -1,44 +1,41 @@ module Neo4j module ActiveNode module Spatial - def self.included(other) - other.extend(ClassMethods) - end - - def add_to_spatial_layer(layer_name = nil) - layer = layer_name || self.class.spatial_layer.name - fail 'layer name not found' unless layer - Neo4j::ActiveBase.current_session.add_node_to_layer(layer, self) - end + extend ActiveSupport::Concern - module ClassMethods - SpatialLayer = Struct.new(:name, :type, :config) + SpatialLayer = Struct.new(:name, :type, :config) - attr_reader :spatial_layer + included do + def add_to_spatial_layer(layer_name = nil) + layer = layer_name || self.class.spatial_layer.name + fail 'layer name not found' unless layer - def spatial_layer(layer_name = nil, options = {}) - @spatial_layer ||= SpatialLayer.new(layer_name, options.fetch(:type, 'SimplePoint'), options.fetch(:config, 'lon:lat')) + Neo4j::ActiveBase.current_session.add_node_to_layer(layer, self) end - def create_layer - fail 'layer not found' unless spatial_layer.name + class << self + attr_reader :spatial_layer - lon_name, lat_name = spatial_layer.config.split(':') + def spatial_layer(layer_name = nil, options = {}) + @spatial_layer ||= SpatialLayer.new(layer_name, options.fetch(:type, 'SimplePoint'), options.fetch(:config, 'lon:lat')) + end - Neo4j::ActiveBase.current_session.add_layer(spatial_layer.name, spatial_layer.type, lat_name, lon_name) - end + def create_layer + fail 'layer not found' unless spatial_layer.name - def remove_layer - fail 'layer not found' unless spatial_layer.name + lon_name, lat_name = spatial_layer.config.split(':') - Neo4j::ActiveBase.current_session.remove_layer(spatial_layer.name) + Neo4j::ActiveBase.current_session.add_layer(spatial_layer.name, spatial_layer.type, lat_name, lon_name) + end + + def remove_layer + fail 'layer not found' unless spatial_layer.name + + Neo4j::ActiveBase.current_session.remove_layer(spatial_layer.name) + end end - end - end - module Query - class QueryProxy - def within_distance(coordinate, distance, layer_name = nil) + scope :within_distance, ->(coordinate, distance, layer_name = nil) do layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session @@ -46,7 +43,7 @@ def within_distance(coordinate, distance, layer_name = nil) .proxy_as(model, :node) end - def bbox(min, max, layer_name = nil) + scope :bbox, ->(min, max, layer_name = nil) do layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session @@ -54,7 +51,7 @@ def bbox(min, max, layer_name = nil) .proxy_as(model, :node) end - def closest(coordinate, distance = 100, layer_name = nil) + scope :closest, ->(coordinate, distance = 100, layer_name = nil) do layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session @@ -62,7 +59,7 @@ def closest(coordinate, distance = 100, layer_name = nil) .proxy_as(model, :node) end - def intersects(geometry, layer_name = nil) + scope :intersects, ->(geometry, layer_name = nil) do layer = model.spatial_layer.name || layer_name Neo4j::ActiveBase.current_session diff --git a/spec/active_node_spatial_spec.rb b/spec/active_node_spatial_spec.rb index 379300e..3193b5e 100644 --- a/spec/active_node_spatial_spec.rb +++ b/spec/active_node_spatial_spec.rb @@ -27,7 +27,7 @@ class Restaurant # let(:match) { Restaurant.all.spatial_match(:r, 'withinDistance:[60.0,15.0,100.0]') } describe '#within_distance' do - let(:match) { Restaurant.all.within_distance({lat: 60.0, lon: 15.0}, 100.0) } + let(:match) { Restaurant.within_distance({lat: 60.0, lon: 15.0}, 100.0) } it 'is a QueryProxy' do expect(match).to respond_to(:to_cypher) @@ -45,7 +45,7 @@ class Restaurant describe '#bbox' do let(:min) { {lat: 59.9, lon: 14.9} } let(:max) { {lat: 60.2, lon: 15.3} } - let(:match) { Restaurant.all.bbox(min, max) } + let(:match) { Restaurant.bbox(min, max) } it 'returns nodes that are inside the given bbox' do nodes = match.to_a @@ -65,7 +65,7 @@ class Restaurant # # this point is closer to outside_node # # let(:coordinate) { {lat: 59.1, lon: 15.0} } # # let(:coordinate) { {lat: 60.0, lon: 15.1} } - # let(:match) { Restaurant.all.closest(coordinate) } + # let(:match) { Restaurant.closest(coordinate) } # # it 'returns the closest node first' do # puts match.to_a @@ -76,7 +76,7 @@ class Restaurant describe '#intersects' do let(:geom) { 'POLYGON ((15.3 60.1, 15.3 58.9, 14.8 58.9, 14.8 60.1, 15.3 60.1))' } - let(:match) { Restaurant.all.intersects(geom) } + let(:match) { Restaurant.intersects(geom) } it 'returns node that intersect the given geometry' do expect(match.count).to eq(2) From b31d9350a65560c66983f030f4bd85a5dd3f46ea Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Thu, 22 Jun 2017 14:04:31 -0700 Subject: [PATCH 11/12] Fix bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + require active support, + fix ‘importShapefileToLayer’ call + improve test cleanup --- lib/neo4j/active_node/spatial.rb | 2 ++ lib/neo4j/spatial.rb | 5 +++-- spec/neo4jrb_spatial_spec.rb | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/neo4j/active_node/spatial.rb b/lib/neo4j/active_node/spatial.rb index fc22788..a87758e 100644 --- a/lib/neo4j/active_node/spatial.rb +++ b/lib/neo4j/active_node/spatial.rb @@ -1,3 +1,5 @@ +require 'active_support/concern' + module Neo4j module ActiveNode module Spatial diff --git a/lib/neo4j/spatial.rb b/lib/neo4j/spatial.rb index c6a844d..f9b92cf 100644 --- a/lib/neo4j/spatial.rb +++ b/lib/neo4j/spatial.rb @@ -115,10 +115,11 @@ def closest(layer, coordinate, distance = 100, execute: true) wrap_spatial_procedure('closest', options, execute: execute) end - def import_shapefile_to_layer(layer, file_uri) + def import_shapefile_to_layer(layer, file_uri, execute: true) options = {layer: layer, file_uri: file_uri} + execution_args = {execute: execute, node: false} - spatial_procedure('importShapefileToLayer', options) + wrap_spatial_procedure('importShapefileToLayer', options, execution_args) end protected diff --git a/spec/neo4jrb_spatial_spec.rb b/spec/neo4jrb_spatial_spec.rb index 5acfd4e..60ee57a 100644 --- a/spec/neo4jrb_spatial_spec.rb +++ b/spec/neo4jrb_spatial_spec.rb @@ -122,6 +122,8 @@ existing_geo = neo.update_from_wkt('zipcodes', geometry, geo) expect(existing_geo.first.props[:wkt]).to eq(geometry) expect(existing_geo.first.neo_id.to_i).to eq(geo.first.neo_id.to_i) + + neo.remove_layer('zipcodes') end end From cec87d591f79c925bb7611f24c23c60303eadc70 Mon Sep 17 00:00:00 2001 From: Tyler Davis Date: Thu, 22 Jun 2017 15:13:14 -0700 Subject: [PATCH 12/12] Update Readme --- README.md | 111 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index da318c7..9eed9b0 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,32 @@ Provides support for Neo4j Spatial to Neo4j.rb 5+. ## Introduction -It is more or less a Neo4j.rb-flavored implementation of [Max De Marzi](https://github.com/maxdemarzi)'s +It was originally more or less a Neo4j.rb-flavored implementation of [Max De Marzi](https://github.com/maxdemarzi)'s [code](https://github.com/maxdemarzi/neography/blob/46be2bb3c66aea14e707b1e6f82937e65f686ccc/lib/neography/rest/spatial.rb) from [Neography](https://github.com/maxdemarzi/neography). +Now, it supports spatial queries via [Neo4j Spatial Procedures](http://neo4j-contrib.github.io/spatial/#spatial-procedures). + For support, open an issue or say hello through [Gitter](https://gitter.im/neo4jrb/neo4j). ## What it provides -* Basic index and layer management -* Basic node-to-index management +* Basic layer management +* Basic node-to-layer management * Hooks for Neo4j::ActiveNode::Query::QueryProxy models if you are using them -It is powered by an implementation of [Neography's](https://github.com/maxdemarzi/neography) [spatial module](https://github.com/maxdemarzi/neography/blob/46be2bb3c66aea14e707b1e6f82937e65f686ccc/lib/neography/rest/spatial.rb). Clearly, a huge debt is owed to [Max De Marzi](https://github.com/maxdemarzi) for doing all the hard work. ## Requirements -* Neo4j-core 5.0.1+ -* Neo4j Server 2.2.2+ (earlier versions will likely work but are not tested) +* Neo4j-core 7.0+ +* Neo4j Server 3.0+ (earlier versions WILL NOT WORK) * Ruby MRI 2.2.2+ * Compatible version of [Neo4j Spatial](https://github.com/neo4j-contrib/spatial) Optionally: -* v5.0.1+ of the [Neo4j gem](https://github.com/neo4jrb/neo4j) +* v8.0.6+ of the [Neo4j gem](https://github.com/neo4jrb/neo4j) # Usage @@ -42,6 +43,8 @@ Optionally: gem 'neo4jrb_spatial', '~> 1.0.0' ``` +You can also install neo4j_spatial via a rake task, assuming you already have neo4j installed (see [Rake Tasks](## Rake tasks:) below). + ## Require it ``` @@ -55,81 +58,104 @@ include Neo4j::ActiveNode::Spatial ## Use it with Neo4j-core ```ruby -# Create an index -Neo4j::Session.current.create_spatial_index('restaurants') +# Create a session object +require 'neo4j/core/cypher_session/adaptors/http' + +neo4j_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new('http://localhost:7474') +session = Neo4j::Core::CypherSession.new(neo4j_adaptor) + +# Create a spatial layer +session.add_layer('restaurants') # Create a node -node = Neo4j::Node.create({:name => "Indie Cafe", :lat => 41.990326, :lon => -87.672907 }, :Restaurant) +properties = {name: "Indie Cafe", lat: 41.990326, lon: -87.672907} +node_query = Neo4j::Core::Query.new(session: session).create(n: {Restaurant: properties}).return(:n) +node = session.query(node_query).first.n + +# Add a node to the layer +session.add_node_to_layer('restaurants', node) -# Add a node to the index -Neo4j::Session.current.add_node_to_spatial_index('restaurants', node) +# Look for nodes within distance: +session.within_distance('restaurants', {lat: 41.99022, lon: -87.6720}, 30).map do |node| + node.props[:name] # node is an instance of Neo4j::Core::Node +end # => ['Indie Cafe'] -# Query around the index -Neo4j::Session.current.query.start('n = node:restaurants({location})').params(location: 'withinDistance:[41.99,-87.67,10.0]').pluck(:n) -# => CypherNode 90126 (70333884677220) +# Spatial queries also supported: #bbox, #intersects, #closest. +# See spec/neo4jrb_spatial_spec.rb for examples. ``` ## Use it with the Neo4j gem - Neo4j.rb does not support legacy indexes, so adding nodes to spatial indexes needs to happen separately from node creation. This is complicated by the fact that Neo4j.rb creates all nodes in transactions, so `after_create` callbacks won't work; instead, add your node to the index once you've confirmed it has been created. + Neo4j.rb does not support legacy indexes, so adding nodes to spatial indexes needs to happen separately from node creation. This is complicated by the fact that Neo4j.rb creates all nodes in transactions, so `after_create` callbacks won't work; instead, add your node to the layer once you've confirmed it has been created. - Start by adding `lat` and `lon` properties to your model. You can also add a `spatial_index` to save yourself some time later. + Start by adding `lat` and `lon` properties to your model. You can also add a `spatial_layer` to save yourself some time later. - ``` + ```ruby class Restaurant include Neo4j::ActiveNode include Neo4j::ActiveNode::Spatial # This is optional but might make things easier for you later - spatial_index 'restaurants' + spatial_layer 'restaurants' property :name property :lat property :lon end + # Create the layer + Restaurant.create_layer + # Create it pizza_hut = Restaurant.create(name: 'Pizza Hut', lat: 60.1, lon: 15.1) # When called without an argument, it will use the value set through `spatial_index` in the model - pizza_hut.add_to_spatial_index + pizza_hut.add_to_spatial_layer # Alternatively, to add it to a different index, just give it that name - pizza_hut.add_to_spatial_index('fake_pizza_places') + pizza_hut.add_to_spatial_layer('fake_pizza_places') ``` -### Manual index addition +### Spatial queries -All of the Neo4j-core spatial methods accept ActiveNode-including nodes, so you can use them as arguments for all defined methods as you would Neo4j::Server::CypherNode instances. +Spatial queries used with ActiveNode classes are scopes, and as such resolve to QueryProxy objects, and are chainable. For example, if you had an `employees` association defined in your model: ```ruby -Neo4j::Session.current.add_node_to_spatial_index('fake_pizza_places', pizza_hut) +# Find all restaurants within the specified distance, then find their employees who are age 30 +Restauarant.within_distance({lat: 60.08, lon: 15.09}, 10).employees.where(age: 30) ``` -### Spatial queries +If you did not define `spatial_layer` on your model, or want to query against something other than the model's default, you can feed a third argument: the layer name to use for the query. -No helpers are provided to query against the REST API -- you'll need to use the ones provided for Neo4j-core; however, a class method is provided to make Cypher queries easier: `spatial_match`. +#### `#bbox` -``` -# Use the index defined on the model as demonstrated above -Restaurant.all.spatial_match(:r, params_string) -# Generates: -# => "START r = node:restaurants({params_string})" +```ruby +# find all restaurants within the bounding box created by the given points: +min = { lat: 59.9, lon: 14.9 } +max = { lat: 60.2, lon: 15.3 } +Restaurant.bbox(min, max) ``` -It then drops you back into a QueryProxy in the context of the class. If you had an `employees` association defined in your model: +#### `#within_distance` - ``` - # Find all restaurants within the specified distance, then find their employees who are age 30 - Restauarant.all.spatial_match(:r, 'withinDistance:[41.99,-87.67,10.0]').employees.where(age: 30) - ``` +```ruby +# find all restaurants within 10km of the given point: +Restauarant.within_distance({lat: 60.08, lon: 15.09}, 10) +``` + +#### `intersects` -If you did no define `spatial_index` on your model or what to query against something other than the model's default, you can feed a third argument: the index to use for the query. +```ruby +# find all restaurants that intersect the given geometry: +geom = 'POLYGON ((15.3 60.1, 15.3 58.9, 14.8 58.9, 14.8 60.1, 15.3 60.1))' +Restauarant.intersects(geom) +``` ## Rake tasks: -#### `neo4j_spatial:install` -usage: `NEO4J_VERSION='3.0.4' rake neo4j_spatial:install[]` +#### `bundle exec rake neo4j_spatial:install` + +usage: `NEO4J_VERSION='3.0.4' bundle exec rake neo4j_spatial:install[]` If no `env` argument is provided, this defaults to 'development' ## Additional Resources @@ -141,9 +167,14 @@ mostly works for an idea of the basics, just replace Neography-specific commands ## Contributions -Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `rubocop` from the CLI. +Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `bundle exec rubocop` from the CLI. ### Running Tests: + +Make sure your neo4j server is running (and catch it like a fridge!): +`bundle exec rake neo4j:start` + +run the test suite: `bundle exec rake spec` or `bundle exec rspec spec` NOTE that if your NEO4J_URL is not the default, you will have to prefix while running migrate: `NEO4J_URL='http://localhost:7123' bundle exec rake spec`