diff --git a/lib/geocoder/sql.rb b/lib/geocoder/sql.rb index 6bca8a65a..b2b19da30 100644 --- a/lib/geocoder/sql.rb +++ b/lib/geocoder/sql.rb @@ -11,6 +11,8 @@ module Sql # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL # def full_distance(latitude, longitude, lat_attr, lon_attr, options = {}) + warn "Direct use of this method is deprecated, prefer Geocoder::Store::ActiveRecord.full_distance_sql" + units = options[:units] || Geocoder.config.units earth = Geocoder::Calculations.earth_radius(units) @@ -32,6 +34,8 @@ def full_distance(latitude, longitude, lat_attr, lon_attr, options = {}) # are not intended for use in production! # def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {}) + warn "Direct use of this method is deprecated, prefer Geocoder::Store::ActiveRecord.approx_distance_sql" + units = options[:units] || Geocoder.config.units dx = Geocoder::Calculations.longitude_degree_distance(30, units) dy = Geocoder::Calculations.latitude_degree_distance(units) @@ -44,6 +48,8 @@ def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {}) end def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr) + warn "Direct use of this method is deprecated, prefer Geocoder::Store::ActiveRecord.within_bounding_box_sql" + spans = "#{lat_attr} BETWEEN #{sw_lat.to_f} AND #{ne_lat.to_f} AND " # handle box that spans 180 longitude if sw_lng.to_f > ne_lng.to_f @@ -66,6 +72,8 @@ def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr) # http://www.beginningspatial.com/calculating_bearing_one_point_another # def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {}) + warn "Direct use of this method is deprecated, prefer Geocoder::Store::ActiveRecord.full_bearing_sql" + degrees_per_radian = Geocoder::Calculations::DEGREES_PER_RADIAN case options[:bearing] || Geocoder.config.distances when :linear @@ -95,6 +103,8 @@ def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {}) # returns *something* in databases without trig functions. # def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {}) + warn "Direct use of this method is deprecated, prefer Geocoder::Store::ActiveRecord.approx_bearing_sql" + "CASE " + "WHEN (#{lat_attr} >= #{latitude.to_f} AND " + "#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " + diff --git a/lib/geocoder/stores/active_record.rb b/lib/geocoder/stores/active_record.rb index be7870c27..1d26b76af 100644 --- a/lib/geocoder/stores/active_record.rb +++ b/lib/geocoder/stores/active_record.rb @@ -63,7 +63,7 @@ def self.included(base) scope :within_bounding_box, lambda{ |*bounds| sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds if sw_lat && sw_lng && ne_lat && ne_lng - where(Geocoder::Sql.within_bounding_box( + where(self.within_bounding_box_sql( sw_lat, sw_lng, ne_lat, ne_lng, full_column_name(geocoder_options[:latitude]), full_column_name(geocoder_options[:longitude]) @@ -138,7 +138,7 @@ def near_scope_options(latitude, longitude, radius = 20, options = {}) full_column_name(latitude_attribute), full_column_name(longitude_attribute) ] - bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args) + bounding_box_conditions = self.within_bounding_box_sql(*args) if using_unextended_sqlite? conditions = bounding_box_conditions @@ -172,8 +172,8 @@ def near_scope_options(latitude, longitude, radius = 20, options = {}) # def distance_sql(latitude, longitude, options = {}) method_prefix = using_unextended_sqlite? ? "approx" : "full" - Geocoder::Sql.send( - method_prefix + "_distance", + self.send( + method_prefix + "_distance_sql", latitude, longitude, full_column_name(options[:latitude] || geocoder_options[:latitude]), full_column_name(options[:longitude]|| geocoder_options[:longitude]), @@ -191,8 +191,8 @@ def bearing_sql(latitude, longitude, options = {}) end if options[:bearing] method_prefix = using_unextended_sqlite? ? "approx" : "full" - Geocoder::Sql.send( - method_prefix + "_bearing", + self.send( + method_prefix + "_bearing_sql", latitude, longitude, full_column_name(options[:latitude] || geocoder_options[:latitude]), full_column_name(options[:longitude]|| geocoder_options[:longitude]), @@ -223,6 +223,134 @@ def select_clause(columns, distance = nil, bearing = nil, distance_column = 'dis clause end + ## + # Distance calculation for use with a database that supports POWER(), + # SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(), + # ATAN2(). + # + # Based on the excellent tutorial at: + # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL + # + def full_distance_sql(latitude, longitude, lat_attr, lon_attr, options = {}) + units = options[:units] || Geocoder.config.units + earth = Geocoder::Calculations.earth_radius(units) + + "#{earth} * 2 * ASIN(SQRT(" + + "POWER(SIN((#{latitude.to_f} - #{lat_attr}) * PI() / 180 / 2), 2) + " + + "COS(#{latitude.to_f} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " + + "POWER(SIN((#{longitude.to_f} - #{lon_attr}) * PI() / 180 / 2), 2)" + + "))" + end + + ## + # Distance calculation for use with a database without trigonometric + # functions, like SQLite. Approach is to find objects within a square + # rather than a circle, so results are very approximate (will include + # objects outside the given radius). + # + # Distance and bearing calculations are *extremely inaccurate*. To be + # clear: this only exists to provide interface consistency. Results + # are not intended for use in production! + # + def approx_distance_sql(latitude, longitude, lat_attr, lon_attr, options = {}) + units = options[:units] || Geocoder.config.units + dx = Geocoder::Calculations.longitude_degree_distance(30, units) + dy = Geocoder::Calculations.latitude_degree_distance(units) + + # sin of 45 degrees = average x or y component of vector + factor = Math.sin(Math::PI / 4) + + "(#{dy} * ABS(#{lat_attr} - #{latitude.to_f}) * #{factor}) + " + + "(#{dx} * ABS(#{lon_attr} - #{longitude.to_f}) * #{factor})" + end + + def within_bounding_box_sql(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr) + spans = "#{lat_attr} BETWEEN #{sw_lat.to_f} AND #{ne_lat.to_f} AND " + # handle box that spans 180 longitude + if sw_lng.to_f > ne_lng.to_f + spans + "(#{lon_attr} BETWEEN #{sw_lng.to_f} AND 180 OR " + + "#{lon_attr} BETWEEN -180 AND #{ne_lng.to_f})" + else + spans + "#{lon_attr} BETWEEN #{sw_lng.to_f} AND #{ne_lng.to_f}" + end + end + + ## + # Fairly accurate bearing calculation. Takes a latitude, longitude, + # and an options hash which must include a :bearing value + # (:linear or :spherical). + # + # For use with a database that supports MOD() and trigonometric functions + # SIN(), COS(), ASIN(), ATAN2(). + # + # Based on: + # http://www.beginningspatial.com/calculating_bearing_one_point_another + # + def full_bearing_sql(latitude, longitude, lat_attr, lon_attr, options = {}) + degrees_per_radian = Geocoder::Calculations::DEGREES_PER_RADIAN + case options[:bearing] || Geocoder.config.distances + when :linear + if using_sqlserver? + "CAST(" + + "(ATN2( " + + "((#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian}), " + + "((#{lat_attr} - #{latitude.to_f}) / #{degrees_per_radian})" + + ") * #{degrees_per_radian}) + 360 " + + "AS decimal) % 360" + else + "MOD(CAST(" + + "(ATAN2( " + + "((#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian}), " + + "((#{lat_attr} - #{latitude.to_f}) / #{degrees_per_radian})" + + ") * #{degrees_per_radian}) + 360 " + + "AS decimal), 360)" + end + when :spherical + if using_sqlserver? + "CAST(" + + "(ATN2( " + + "SIN( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian} ) * " + + "COS( (#{lat_attr}) / #{degrees_per_radian} ), (" + + "COS( (#{latitude.to_f}) / #{degrees_per_radian} ) * SIN( (#{lat_attr}) / #{degrees_per_radian})" + + ") - (" + + "SIN( (#{latitude.to_f}) / #{degrees_per_radian}) * COS((#{lat_attr}) / #{degrees_per_radian}) * " + + "COS( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian})" + + ")" + + ") * #{degrees_per_radian}) + 360 " + + "AS decimal) % 360" + else + "MOD(CAST(" + + "(ATAN2( " + + "SIN( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian} ) * " + + "COS( (#{lat_attr}) / #{degrees_per_radian} ), (" + + "COS( (#{latitude.to_f}) / #{degrees_per_radian} ) * SIN( (#{lat_attr}) / #{degrees_per_radian})" + + ") - (" + + "SIN( (#{latitude.to_f}) / #{degrees_per_radian}) * COS((#{lat_attr}) / #{degrees_per_radian}) * " + + "COS( (#{lon_attr} - #{longitude.to_f}) / #{degrees_per_radian})" + + ")" + + ") * #{degrees_per_radian}) + 360 " + + "AS decimal), 360)" + end + end + end + + ## + # Totally lame bearing calculation. Basically useless except that it + # returns *something* in databases without trig functions. + # + def approx_bearing_sql(latitude, longitude, lat_attr, lon_attr, options = {}) + "CASE " + + "WHEN (#{lat_attr} >= #{latitude.to_f} AND " + + "#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " + + "WHEN (#{lat_attr} < #{latitude.to_f} AND " + + "#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " + + "WHEN (#{lat_attr} < #{latitude.to_f} AND " + + "#{lon_attr} < #{longitude.to_f}) THEN 225.0 " + + "WHEN (#{lat_attr} >= #{latitude.to_f} AND " + + "#{lon_attr} < #{longitude.to_f}) THEN 315.0 " + + "END" + end + ## # Adds a condition to exclude a given object by ID. # Expects conditions as an array or string. Returns array. @@ -256,6 +384,11 @@ def using_postgres? connection.adapter_name.match(/postgres/i) end + # SQL Server unhelpfully defines ATAN2 as ATN2 + def using_sqlserver? + !!connection.adapter_name.match(/sqlserver/i) + end + ## # Use OID type when running in PosgreSQL # @@ -263,6 +396,12 @@ def null_value using_postgres? ? 'NULL::text' : 'NULL' end + # When using sql server, use ATN2 + # For effectively all other DBs + def atan2_alias + using_sqlserver ? 'ATN2' : 'ATAN2' + end + ## # Value which can be passed to where() to produce no results. #