From 76d663530d18f1f5b52b6a3ee00d0fd6badad85b Mon Sep 17 00:00:00 2001
From: Alex Reisner <alex@alexreisner.com>
Date: Sat, 29 Sep 2012 10:50:01 -0400
Subject: [PATCH] Move SQL calculation methods to new module.

---
 lib/geocoder/sql.rb                  |  90 +++++++++++++++
 lib/geocoder/stores/active_record.rb | 160 ++++++++-------------------
 2 files changed, 134 insertions(+), 116 deletions(-)
 create mode 100644 lib/geocoder/sql.rb

diff --git a/lib/geocoder/sql.rb b/lib/geocoder/sql.rb
new file mode 100644
index 00000000..e5877561
--- /dev/null
+++ b/lib/geocoder/sql.rb
@@ -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
diff --git a/lib/geocoder/stores/active_record.rb b/lib/geocoder/stores/active_record.rb
index cc315abe..c57ac8b6 100644
--- a/lib/geocoder/stores/active_record.rb
+++ b/lib/geocoder/stores/active_record.rb
@@ -1,3 +1,4 @@
+require 'geocoder/sql'
 require 'geocoder/stores/base'
 
 ##
@@ -43,7 +44,6 @@ module Geocoder::Store
           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 @@ module Geocoder::Store
         #
         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,19 +98,22 @@ module Geocoder::Store
       # * +: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
 
       ##
@@ -120,18 +121,18 @@ module Geocoder::Store
       # 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),
@@ -139,37 +140,6 @@ module Geocoder::Store
         )
       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
@@ -180,79 +150,36 @@ module Geocoder::Store
       # 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.
       #
@@ -279,9 +206,10 @@ module Geocoder::Store
 
       ##
       # 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
-- 
GitLab