Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Support for SQL Server (WIP for discussion) #1605

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/geocoder/sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 " +
Expand Down
151 changes: 145 additions & 6 deletions lib/geocoder/stores/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]),
Expand All @@ -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]),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -256,13 +384,24 @@ 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
#
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.
#
Expand Down