From b3d27f6403e093a1d3532e630ac8665852100409 Mon Sep 17 00:00:00 2001 From: mjy Date: Thu, 3 Jul 2014 13:36:33 -0500 Subject: [PATCH] Geographic items refactoring experiments. --- app/models/geographic_item.rb | 63 +++++++++++++++++++++------- spec/models/collecting_event_spec.rb | 2 +- spec/models/geographic_item_spec.rb | 6 ++- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/models/geographic_item.rb b/app/models/geographic_item.rb index de44c8450c..e6ffedddb2 100644 --- a/app/models/geographic_item.rb +++ b/app/models/geographic_item.rb @@ -94,10 +94,6 @@ def st_npoints GeographicItem.where(id: self.id).select("ST_NPoints(#{self.st_as_binary}) number_points").first['number_points'].to_i end - def st_as_binary - "ST_AsBinary(#{self.geo_object_type})" - end - def parent_geographic_areas self.geographic_areas.collect { |a| a.parent } end @@ -161,13 +157,22 @@ def center_coords st_centroid end + def st_as_binary + "ST_AsBinary(#{self.geo_object_type})" + end + # TODO: Find ST_Centroid(g1) method and # Return an Array of [latitude, longitude] for the centroid of GeoItem def st_centroid # GeographicItem.where(id: self.id).select("ST_NPoints(#{self.st_as_binary}) number_points").first['number_points'].to_i - GeographicItem.where(id: self.id).select("st_astext(st_centroid(st_geomfromewkb(#{self.st_as_binary})") + GeographicItem.where(id: self.id).select( "id, st_astext(st_centroid( #{to_geometry_sql} )) centroid" ).first + end + + def to_geometry_sql + "ST_GeomFromEWKB( #{self.geo_object_type} )" end + =begin scope :intersecting_boxes, -> (column_name, geographic_item) { select("ST_Contains(geographic_items.#{column_name}, #{geographic_item.geo_object})", @@ -240,13 +245,14 @@ def intersecting(column_name, *geographic_items) else q = geographic_items.flatten.collect { |geographic_item| - "ST_Intersects(#{column_name}, 'srid=4326;#{geographic_item.geo_object}')" # seems like we want this: http://danshultz.github.io/talks/mastering_activerecord_arel/#/15/2 + "ST_Intersects(#{column_name}, '#{geographic_item.geo_object}' )" # seems like we want this: http://danshultz.github.io/talks/mastering_activerecord_arel/#/15/2 }.join(' or ') where(q) end - end + end # end class << self + def st_intersects(column_name = :multi_polygon, geometry) geographic_item = GeographicItem.arel_table @@ -264,25 +270,52 @@ def st_intersects(column_name = :multi_polygon, geometry) =end end - # TODO: Document, what units are distance in? - # todo: distance is measured in meters - def self.within_radius_of(column_name, geographic_item, distance) + + # Trying with Arel + # setup an Arel table + # append conditions with each loop to the table + # project the result + # geographic_items = GeographicItem.arel_table + # conditions = [] + # st_distances = [] + + # g1 = geographic_items.alias + # g1[:id].eq(geographic_item.id).project(column) + + # DATA_TYPES.each do |column| + # + # g2 = geographic_items.where(geographic_items[:id].eq(geographic_item.id)) + # g3 = g2[column] + + # a = Arel::Nodes::NamedFunction.new("st_distance", [ geographic_items[column.to_sym], g2 ] ) + # byebug + # conditions.push(where(a.lt(distance))) + # end + + + # distance is measured in meters + def self.within_radius_of(column_name, geographic_item, distance) # ultimately it should be geographic_item_id if column_name.downcase == 'any' partial = [] + DATA_TYPES.each { |column| - partial.push(GeographicItem.within_radius_of("#{column}", geographic_item, distance).to_a) + partial.push(GeographicItem.within_radius_of("#{column}", geographic_item, distance)) } - # todo: change 'id in (?)' to some other sql construct - GeographicItem.where(id: partial.flatten.map(&:id)) + + GeographicItem.where(id: partial.flatten.map(&:id) ) else if check_geo_params(column_name, geographic_item) - where ("st_distance(#{column_name}, GeomFromEWKT('srid=4326;#{geographic_item.geo_object}')) < #{distance}") + where("st_distance(#{column_name}, #{ select_geography_sql(geographic_item) }) < #{distance}") else - where ('false') + where("false") end end end + def self.select_geography_sql(geographic_item) + "(SELECT #{geographic_item.geo_object_type} from geographic_items where id = #{geographic_item.to_param})" + end + def self.disjoint_from(column_name, *geographic_items) q = geographic_items.flatten.collect { |geographic_item| "st_disjoint(#{column_name}::geometry, GeomFromEWKT('srid=4326;#{geographic_item.geo_object}'))" diff --git a/spec/models/collecting_event_spec.rb b/spec/models/collecting_event_spec.rb index aa69855d6a..84c51efb00 100644 --- a/spec/models/collecting_event_spec.rb +++ b/spec/models/collecting_event_spec.rb @@ -293,7 +293,7 @@ # The idea: # - geopolitical names all come from GeographicArea, as classified by GeographicAreaType # - we can arrive at a geographic_area from a collecting event in 2 ways - # 1) @collecting_event.geographic_area is set, this is easy + # 1) @collecting_event.geographic_area is set, this is easy, we can use it specifically if it's the right type, or climb up to a specific levelN category to check if not # 2) @collecting_event.georeferences.first is set. # In the case of 2) we must use the georeference to find the minimum containing geographic_area of type "state" for example # - it is possible (but hopefully unlikely) that multiple geographic areas of type "state" might be return, diff --git a/spec/models/geographic_item_spec.rb b/spec/models/geographic_item_spec.rb index 88fffc274e..870680a1b1 100644 --- a/spec/models/geographic_item_spec.rb +++ b/spec/models/geographic_item_spec.rb @@ -589,7 +589,7 @@ specify '#st_centroid returns a lat/lng of the centroid of the GeoObject' do # select st_centroid('multipoint (-4.0 4.0 0.0, 4.0 4.0 0.0, 4.0 -4.0 0.0, -4.0 -4.0 0.0)'); - expect(@area_d.st_centroid).to eq([1.0, 0.0]) + expect(@area_d.st_centroid['centroid'].to_s).to eq('POINT (0.0 0.0 0.0)') end specify '#start_point returns a lat/lng of the first point of the GeoObject' do @@ -772,6 +772,10 @@ expect(GeographicItem.within_radius_of('polygon', @p0, 1000000)).to eq([@e2, @e3, @e4, @e5, @area_a, @area_b, @area_c, @area_d]) end + skip '::within_radius("any"...)' do + # expect(GeographicItem.within_radius_of('any', @p0, 1000000)).to eq([@e2, @e3, @e4, @e5, @area_a, @area_b, @area_c, @area_d]) + end + specify "::intersecting list of objects (uses 'or')" do expect(GeographicItem.intersecting('polygon', [@l])).to eq([@k]) expect(GeographicItem.intersecting('polygon', [@f1])).to eq([]) # Is this right?