Permalink
Browse files

Move SQL calculation methods to new module.

  • Loading branch information...
1 parent a0e771a commit 76d663530d18f1f5b52b6a3ee00d0fd6badad85b @alexreisner committed Sep 29, 2012
Showing with 134 additions and 116 deletions.
  1. +90 −0 lib/geocoder/sql.rb
  2. +44 −116 lib/geocoder/stores/active_record.rb
View
@@ -0,0 +1,90 @@
+module Geocoder
+ module Sql
+ extend self
+
+ ##
+ # Distance calculation based on the excellent tutorial at:
+ # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
+ #
+ def full_distance(latitude, longitude, lat_attr, lon_attr, options = {})
+ earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
+
+ "#{earth} * 2 * ASIN(SQRT(" +
+ "POWER(SIN((#{latitude} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
+ "COS(#{latitude} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
+ "POWER(SIN((#{longitude} - #{lon_attr}) * PI() / 180 / 2), 2)" +
+ "))"
+ end
+
+ def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {})
+ dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
+ dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
+
+ # sin of 45 degrees = average x or y component of vector
+ factor = Math.sin(Math::PI / 4)
+
+ "(#{dy} * ABS(#{lat_attr} - #{latitude}) * #{factor}) + " +
+ "(#{dx} * ABS(#{lon_attr} - #{longitude}) * #{factor})"
+ end
+
+ def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
+ spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
+ # handle box that spans 180 longitude
+ if sw_lng.to_f > ne_lng.to_f
+ spans + "#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
+ "#{lon_attr} BETWEEN -180 AND #{ne_lng}"
+ else
+ spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
+ end
+ end
+
+ ##
+ # Fairly accurate bearing calculation. Takes a latitude, longitude,
+ # and an options hash which must include a :bearing value
+ # (:linear or :spherical).
+ #
+ # Based on:
+ # http://www.beginningspatial.com/calculating_bearing_one_point_another
+ #
+ def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
+ case options[:bearing]
+ when :linear
+ "CAST(" +
+ "DEGREES(ATAN2( " +
+ "RADIANS(#{lon_attr} - #{longitude}), " +
+ "RADIANS(#{lat_attr} - #{latitude})" +
+ ")) + 360 " +
+ "AS decimal) % 360"
+ when :spherical
+ "CAST(" +
+ "DEGREES(ATAN2( " +
+ "SIN(RADIANS(#{lon_attr} - #{longitude})) * " +
+ "COS(RADIANS(#{lat_attr})), (" +
+ "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{lat_attr}))" +
+ ") - (" +
+ "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{lat_attr})) * " +
+ "COS(RADIANS(#{lon_attr} - #{longitude}))" +
+ ")" +
+ ")) + 360 " +
+ "AS decimal) % 360"
+ end
+ end
+
+ ##
+ # Totally lame bearing calculation. Basically useless except that it
+ # returns *something* in databases without trig functions.
+ #
+ def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
+ "CASE " +
+ "WHEN (#{lat_attr} >= #{latitude} AND " +
+ "#{lon_attr} >= #{longitude}) THEN 45.0 " +
+ "WHEN (#{lat_attr} < #{latitude} AND " +
+ "#{lon_attr} >= #{longitude}) THEN 135.0 " +
+ "WHEN (#{lat_attr} < #{latitude} AND " +
+ "#{lon_attr} < #{longitude}) THEN 225.0 " +
+ "WHEN (#{lat_attr} >= #{latitude} AND " +
+ "#{lon_attr} < #{longitude}) THEN 315.0 " +
+ "END"
+ end
+ end
+end
@@ -1,3 +1,4 @@
+require 'geocoder/sql'
require 'geocoder/stores/base'
##
@@ -43,7 +44,6 @@ def self.included(base)
end
}
-
##
# Find all objects within the area of a given bounding box.
# Bounds must be an array of locations specifying the southwest
@@ -52,17 +52,15 @@ def self.included(base)
#
scope :within_bounding_box, lambda{ |bounds|
sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
- unless sw_lat && sw_lng && ne_lat && ne_lng
- return select(select_clause(nil, "NULL", "NULL")).where(false_condition)
- end
- spans = "#{geocoder_options[:latitude]} BETWEEN #{sw_lat} AND #{ne_lat} AND "
- spans << if sw_lng.to_f > ne_lng.to_f # handle box that spans 180 longitude
- "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND 180 OR " +
- "#{geocoder_options[:longitude]} BETWEEN -180 AND #{ne_lng}"
+ if sw_lat && sw_lng && ne_lat && ne_lng
+ {:conditions => Geocoder::Sql.within_bounding_box(
+ sw_lat, sw_lng, ne_lat, ne_lng,
+ full_column_name(geocoder_options[:latitude]),
+ full_column_name(geocoder_options[:longitude])
+ )}
else
- "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND #{ne_lng}"
+ select(select_clause(nil, "NULL", "NULL")).where(false_condition)
end
- { :conditions => spans }
}
end
end
@@ -100,38 +98,41 @@ def distance_from_sql(location, *args)
# * +:exclude+ - an object to exclude (used by the +nearbys+ method)
#
def near_scope_options(latitude, longitude, radius = 20, options = {})
- if using_sqlite?
- approx_near_scope_options(latitude, longitude, radius, options)
- else
- full_near_scope_options(latitude, longitude, radius, options)
- end
+ method_prefix = using_sqlite? ? "approx" : "full"
+ send(
+ method_prefix + "_near_scope_options",
+ latitude, longitude, radius, options
+ )
end
def distance_from_sql_options(latitude, longitude, options = {})
- if using_sqlite?
- approx_distance_from_sql(latitude, longitude, options)
- else
- full_distance_from_sql(latitude, longitude, options)
- end
+ method_prefix = using_sqlite? ? "approx" : "full"
+ Geocoder::Sql.send(
+ method_prefix + "_distance",
+ latitude, longitude,
+ full_column_name(geocoder_options[:latitude]),
+ full_column_name(geocoder_options[:longitude]),
+ options
+ )
end
##
# Scope options hash for use with a database that supports POWER(),
# SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
# ATAN2(), DEGREES(), and RADIANS().
#
- # Bearing calculation based on:
- # http://www.beginningspatial.com/calculating_bearing_one_point_another
- #
def full_near_scope_options(latitude, longitude, radius, options)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
options[:bearing] ||= (options[:method] ||
geocoder_options[:method] ||
Geocoder::Configuration.distances)
- bearing = full_bearing_sql(latitude, longitude, options)
+ bearing = Geocoder::Sql.full_bearing(
+ latitude, longitude,
+ full_column_name(geocoder_options[:latitude]),
+ full_column_name(geocoder_options[:longitude]),
+ options
+ )
options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
- distance = full_distance_from_sql(latitude, longitude, options)
+ distance = distance_from_sql_options(latitude, longitude, options)
conditions = ["#{distance} <= ?", radius]
default_near_scope_options(latitude, longitude, radius, options).merge(
:select => select_clause(options[:select], distance, bearing),
@@ -140,37 +141,6 @@ def full_near_scope_options(latitude, longitude, radius, options)
end
##
- # Distance calculation based on the excellent tutorial at:
- # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
- #
- def full_distance_from_sql(latitude, longitude, options)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
-
- earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
-
- "#{earth} * 2 * ASIN(SQRT(" +
- "POWER(SIN((#{latitude} - #{full_column_name(lat_attr)}) * PI() / 180 / 2), 2) + " +
- "COS(#{latitude} * PI() / 180) * COS(#{full_column_name(lat_attr)} * PI() / 180) * " +
- "POWER(SIN((#{longitude} - #{full_column_name(lon_attr)}) * PI() / 180 / 2), 2)" +
- "))"
- end
-
- def approx_distance_from_sql(latitude, longitude, options)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
-
- dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
- dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
-
- # sin of 45 degrees = average x or y component of vector
- factor = Math.sin(Math::PI / 4)
-
- "(#{dy} * ABS(#{full_column_name(lat_attr)} - #{latitude}) * #{factor}) + " +
- "(#{dx} * ABS(#{full_column_name(lon_attr)} - #{longitude}) * #{factor})"
- end
-
- ##
# Scope options hash 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
@@ -180,80 +150,37 @@ def approx_distance_from_sql(latitude, longitude, options)
# only exist for interface consistency--not intended for production!
#
def approx_near_scope_options(latitude, longitude, radius, options)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
unless options.include?(:bearing)
options[:bearing] = (options[:method] || \
geocoder_options[:method] || \
Geocoder::Configuration.distances)
end
- bearing = options[:bearing] ?
- approx_bearing_sql(latitude, longitude) : false
+ if options[:bearing]
+ bearing = Geocoder::Sql.approx_bearing(
+ latitude, longitude,
+ full_column_name(geocoder_options[:latitude]),
+ full_column_name(geocoder_options[:longitude])
+ )
+ else
+ bearing = false
+ end
options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
- distance = approx_distance_from_sql(latitude, longitude, options)
+ distance = distance_from_sql_options(latitude, longitude, options)
b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
- conditions = [
- "#{full_column_name(lat_attr)} BETWEEN ? AND ? AND #{full_column_name(lon_attr)} BETWEEN ? AND ?"] +
- [b[0], b[2], b[1], b[3]
+ args = b + [
+ full_column_name(geocoder_options[:latitude]),
+ full_column_name(geocoder_options[:longitude])
]
+ conditions = Geocoder::Sql.within_bounding_box(*args)
default_near_scope_options(latitude, longitude, radius, options).merge(
:select => select_clause(options[:select], distance, bearing),
:conditions => add_exclude_condition(conditions, options[:exclude])
)
end
##
- # SQL for bearing calculation. Takes a latitude, longitude, and an
- # options has which must include a :bearing value
- # (:linear or :spherical).
- #
- def full_bearing_sql(latitude, longitude, options)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
- case options[:bearing]
- when :linear
- "CAST(" +
- "DEGREES(ATAN2( " +
- "RADIANS(#{full_column_name(lon_attr)} - #{longitude}), " +
- "RADIANS(#{full_column_name(lat_attr)} - #{latitude})" +
- ")) + 360 " +
- "AS decimal) % 360"
- when :spherical
- "CAST(" +
- "DEGREES(ATAN2( " +
- "SIN(RADIANS(#{full_column_name(lon_attr)} - #{longitude})) * " +
- "COS(RADIANS(#{full_column_name(lat_attr)})), (" +
- "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{full_column_name(lat_attr)}))" +
- ") - (" +
- "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{full_column_name(lat_attr)})) * " +
- "COS(RADIANS(#{full_column_name(lon_attr)} - #{longitude}))" +
- ")" +
- ")) + 360 " +
- "AS decimal) % 360"
- end
- end
-
- ##
- # SQL for really lame bearing calculation.
- #
- def approx_bearing_sql(latitude, longitude)
- lat_attr = geocoder_options[:latitude]
- lon_attr = geocoder_options[:longitude]
- "CASE " +
- "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND " +
- "#{full_column_name(lon_attr)} >= #{longitude}) THEN 45.0 " +
- "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND " +
- "#{full_column_name(lon_attr)} >= #{longitude}) THEN 135.0 " +
- "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND " +
- "#{full_column_name(lon_attr)} < #{longitude}) THEN 225.0 " +
- "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND " +
- "#{full_column_name(lon_attr)} < #{longitude}) THEN 315.0 " +
- "END"
- end
-
- ##
# Generate the SELECT clause.
#
def select_clause(columns, distance, bearing)
@@ -279,9 +206,10 @@ def default_near_scope_options(latitude, longitude, radius, options)
##
# Adds a condition to exclude a given object by ID.
- # The given conditions MUST be an array.
+ # Expects conditions as an array or string. Returns array.
#
def add_exclude_condition(conditions, exclude)
+ conditions = [conditions] if conditions.is_a?(String)
if exclude
conditions[0] << " AND #{full_column_name(primary_key)} != ?"
conditions << exclude.id

0 comments on commit 76d6635

Please sign in to comment.