diff --git a/lib/geocoder/calculations.rb b/lib/geocoder/calculations.rb index b99e35f0a6add04230c7ff5e0dbacfcd32ac42af..b617bad206abfacbd543ae825545830036ce070f 100644 --- a/lib/geocoder/calculations.rb +++ b/lib/geocoder/calculations.rb @@ -2,6 +2,14 @@ module Geocoder module Calculations extend self + ## + # Compass point names, listed clockwise starting at North. + # + # If you want bearings named using more, fewer, or different points + # override Geocoder::Calculations.COMPASS_POINTS with your own array. + # + COMPASS_POINTS = %w[N NE E SE S SW W NW] + ## # Calculate the distance between two points on Earth (Haversine formula). # Takes two sets of coordinates and an options hash: @@ -32,6 +40,29 @@ module Geocoder c * conversions[options[:units]] end + ## + # Calculate bearing between two sets of co-ordinates. + # Returns a number of degrees from due north (clockwise). + # + # Based on: http://www.movable-type.co.uk/scripts/latlong.html + # + def bearing_between(lat1, lon1, lat2, lon2) + dlon = to_radians((lon1 - lon2).abs) + y = Math.sin(dlon) * Math.cos(lat2) + x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * + Math.cos(lat2) * Math.cos(dlon) + brng = Math.atan2(x,y) + (to_degrees(brng) + 360) % 360 + end + + ## + # Translate a bearing (float) into a compass direction (string, eg "North"). + # + def compass_point(bearing, points = COMPASS_POINTS) + seg_size = 360 / points.size + points[((bearing + (seg_size / 2)) % 360) / seg_size] + end + ## # Compute the geographic center (aka geographic midpoint, center of # gravity) for an array of geocoded objects and/or [lat,lon] arrays @@ -88,35 +119,5 @@ module Geocoder def km_in_mi 0.621371192 end - - ## - # Calculate bearing between two sets of co-ordinates - # - def bearing_between(lat1, lon1, lat2, lon2, options = {}) - # Math courtesy of http://www.movable-type.co.uk/scripts/latlong.html - dlon = to_radians((lon1 - lon2).abs) - - y = Math.sin(dlon) * Math.cos(lat2) - x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dlon) - brng = Math.atan2(x,y) - (to_degrees(brng) + 360) % 360 - end - - # If you want more or fewer points simply override Geocoder::Calculations.COMPASS_POINTS with your own array - COMPASS_POINTS = [{:name => "North", :abbr => "N"}, - {:name => "North East", :abbr => "NE"}, - {:name => "East", :abbr => "E"}, - {:name => "South East", :abbr => "SE"}, - {:name => "South", :abbr => "S"}, - {:name => "South West", :abbr => "SW"}, - {:name => "West", :abbr => "W"}, - {:name => "North West", :abbr => "NW"}] - - ## - # Compass direction (North, South, etc.) between two sets of co-ordinates - def compass_point(bearing, points = COMPASS_POINTS) - seg_size = 360/points.length - points[((bearing + (seg_size/2) ) % 360) / seg_size] - end end end diff --git a/lib/geocoder/orms/active_record.rb b/lib/geocoder/orms/active_record.rb index 0481f307cc5526f5854089fdef8c5c84517ff198..2633fb27e0a55d3f2eea41efc3581de010568385 100644 --- a/lib/geocoder/orms/active_record.rb +++ b/lib/geocoder/orms/active_record.rb @@ -59,7 +59,6 @@ module Geocoder::Orm # +limit+ :: number of records to return (for LIMIT SQL clause) # +offset+ :: number of records to skip (for OFFSET SQL clause) # +select+ :: string with the SELECT SQL fragment (e.g. “id, nameâ€) - # +bearing+ :: :line for straight line calcs, :sphere for spherical # def near_scope_options(latitude, longitude, radius = 20, options = {}) radius *= Geocoder::Calculations.km_in_mi if options[:units] == :km @@ -77,30 +76,33 @@ module Geocoder::Orm # Scope options hash for use with a database that supports POWER(), # SQRT(), PI(), and trigonometric functions (SIN(), COS(), and ASIN()). # - # Taken from the excellent tutorial at: + # Distance calculations based on the excellent tutorial at: # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL # + # 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] - bearing = case options[:bearing] - # Credit for SQL query (adapted) http://www.beginningspatial.com/calculating_bearing_one_point_another - when :line - ", (DEGREES(ATAN2(( longitude - #{longitude} ), ( latitude - #{latitude} )))+360) % 360 AS bearing" - when :spherical - ", (DEGREES( ATAN2( SIN(RADIANS(longitude - #{longitude})) * COS(RADIANS(latitude)), ( COS(RADIANS(#{latitude})) * SIN(RADIANS(latitude)) ) - ( SIN(RADIANS(#{latitude})) * COS(RADIANS(latitude)) * COS(RADIANS(longitude - #{longitude})) ) )) + 360 ) % 360 AS bearing" - else - "" - end + bearing = "(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) % 360" distance = "3956 * 2 * ASIN(SQRT(" + "POWER(SIN((#{latitude} - #{lat_attr}) * " + - "PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " + + "PI() / 180 / 2), 2) + COS(#{latitude} * PI() / 180) * " + "COS(#{lat_attr} * PI() / 180) * " + "POWER(SIN((#{longitude} - #{lon_attr}) * " + "PI() / 180 / 2), 2) ))" options[:order] ||= "#{distance} ASC" default_near_scope_options(latitude, longitude, radius, options).merge( - :select => "#{options[:select] || '*'}, #{distance} AS distance#{bearing}", + :select => "#{options[:select] || '*'}, " + + "#{distance} AS distance, #{bearing} AS bearing", :having => "#{distance} <= #{radius}" ) end diff --git a/test/geocoder_test.rb b/test/geocoder_test.rb index 458b59ee55683f13a7b6f325a4eefd9a2f857835..80c7255a3643bd0c54c9f1fb0c79b42a79f53d52 100644 --- a/test/geocoder_test.rb +++ b/test/geocoder_test.rb @@ -24,16 +24,14 @@ class GeocoderTest < Test::Unit::TestCase end def test_distance_between - hash_north = {:name => "North", :abbr => "N"} - hash_south = {:name => "South", :abbr => "S"} - hash_nw = {:name => "North West", :abbr => "NW"} - assert_equal hash_north, Geocoder::Calculations.compass_point(0) - assert_equal hash_north, Geocoder::Calculations.compass_point(360) - assert_equal hash_north, Geocoder::Calculations.compass_point(361) - assert_equal hash_north, Geocoder::Calculations.compass_point(-22) - assert_equal hash_nw, Geocoder::Calculations.compass_point(-23) - assert_equal hash_south, Geocoder::Calculations.compass_point(180) - assert_equal hash_south, Geocoder::Calculations.compass_point(181) + assert_equal "N", Geocoder::Calculations.compass_point(0) + assert_equal "N", Geocoder::Calculations.compass_point(1.0) + assert_equal "N", Geocoder::Calculations.compass_point(360) + assert_equal "N", Geocoder::Calculations.compass_point(361) + assert_equal "N", Geocoder::Calculations.compass_point(-22) + assert_equal "NW", Geocoder::Calculations.compass_point(-23) + assert_equal "S", Geocoder::Calculations.compass_point(180) + assert_equal "S", Geocoder::Calculations.compass_point(181) end def test_geographic_center_with_arrays