Skip to content
Snippets Groups Projects
Commit b956f0c5 authored by Alex Reisner's avatar Alex Reisner
Browse files

Add options for setting custom latitude and longitude attributes.

parent 03f8e549
No related branches found
No related tags found
No related merge requests found
...@@ -12,9 +12,17 @@ To add geocoding features to a class: ...@@ -12,9 +12,17 @@ To add geocoding features to a class:
geocoded_by :location geocoded_by :location
Be sure your class defines read/write attributes +latitude+ and +longitude+ as well as a method called +location+ (or whatever name you pass to +geocoded_by+) that returns a string suitable for passing to a Google Maps search, for example: Be sure your class defines attributes for storing latitude and longitude (use float or double database columns) and a location (human-readable address to be geocoded). These attribute names are all configurable, for example:
geocoded_by :address,
:latitude => :lat,
:longitude => :lon
A geocodable string is basically anything you'd use to search Google Maps. Any of the following are acceptable:
714 Green St, Big Town, MO 714 Green St, Big Town, MO
Eiffel Tower, Paris, FR
Paris, TX, US
If your model has +address+, +city+, +state+, and +country+ attributes your +location+ method might look something like this: If your model has +address+, +city+, +state+, and +country+ attributes your +location+ method might look something like this:
...@@ -23,21 +31,32 @@ If your model has +address+, +city+, +state+, and +country+ attributes your +loc ...@@ -23,21 +31,32 @@ If your model has +address+, +city+, +state+, and +country+ attributes your +loc
end end
== Examples == Features
Assuming +Venue+ is a geocoded model:
Look up coordinates of an object: Venue.find_near('Omaha, NE, US', 20) # venues within 20 miles of Omaha
Venue.geocoded # venues with coordinates
Venue.not_geocoded # venues without coordinates
obj.fetch_coordinates # returns an array [lat, lon] Assuming +obj+ has a valid string for its +location+:
obj.fetch_and_assign_coordinates # writes values to +latitude+ and +longitude+
obj.fetch_coordinates # returns coordinates [lat, lon]
obj.fetch_and_assign_coordinates # writes values to object
Find distance between object and a point: Find distance between object and a point:
obj.distance_to(40.71432, -100.23487) # in miles obj.distance_to(40.71432, -100.23487) # in miles
obj.distance_to(40.71432, -100.23487, :km) # in kilometers obj.distance_to(40.71432, -100.23487, :km) # in kilometers
Some utility methods are also available:
Find objects within 20 miles of a point: # distance (in miles) between Eiffel Tower and Empire State Building
Geocoder.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
# look up coordinates of some location (like searching Google Maps)
Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
Venue.find_near('Omaha, NE, US', 20) # Venue is a geocoded model
Please see the code for more methods and detailed information about arguments. Please see the code for more methods and detailed information about arguments.
......
...@@ -4,8 +4,10 @@ ActiveRecord::Base.class_eval do ...@@ -4,8 +4,10 @@ ActiveRecord::Base.class_eval do
# Include the Geocoder module and set the method name which returns # Include the Geocoder module and set the method name which returns
# the geo-search string. # the geo-search string.
# #
def self.geocoded_by(method_name = :location) def self.geocoded_by(method_name = :location, options = {})
include Geocoder include Geocoder
@geocoder_method_name = method_name @geocoder_method_name = method_name
@geocoder_latitude_attr = options[:latitude] || :latitude
@geocoder_longitude_attr = options[:longitude] || :longitude
end end
end end
...@@ -20,25 +20,6 @@ module Geocoder ...@@ -20,25 +20,6 @@ module Geocoder
end end
end end
##
# Query Google for the coordinates of the given phrase.
# Returns array [lat,lon] if found, nil if not found or if network error.
#
def self.fetch_coordinates(query)
return nil unless doc = self.search(query)
# Make sure search found a result.
e = doc.elements['kml/Response/Status/code']
return nil unless (e and e.text == "200")
# Isolate the relevant part of the result.
place = doc.elements['kml/Response/Placemark']
# If there are multiple results, blindly use the first.
coords = place.elements['Point/coordinates'].text
coords.split(',')[0...2].reverse.map{ |i| i.to_f }
end
## ##
# Methods which will be class methods of the including class. # Methods which will be class methods of the including class.
# #
...@@ -53,14 +34,81 @@ module Geocoder ...@@ -53,14 +34,81 @@ module Geocoder
latitude, longitude = location.is_a?(Array) ? latitude, longitude = location.is_a?(Array) ?
location : Geocoder.fetch_coordinates(location) location : Geocoder.fetch_coordinates(location)
return [] unless (latitude and longitude) return [] unless (latitude and longitude)
all(Geocoder.find_near_options(latitude, longitude, radius, options)) all(find_near_options(latitude, longitude, radius, options))
end end
## ##
# Get the name of the method that returns the search string. # Get options hash suitable for passing to ActiveRecord.find to get
# records within a radius (in miles) of the given point.
# Taken from excellent tutorial at:
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
#
# Options hash may include:
#
# +order+ :: column(s) for ORDER BY SQL clause
# +limit+ :: number of records to return (for LIMIT SQL clause)
# +offset+ :: number of records to skip (for LIMIT SQL clause)
#
def find_near_options(latitude, longitude, radius = 20, options = {})
# Set defaults/clean up arguments.
options[:order] ||= 'distance ASC'
radius = radius.to_i
# Constrain search to a (radius x radius) square.
factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
lon_lo = longitude - (radius / factor);
lon_hi = longitude + (radius / factor);
lat_lo = latitude - (radius / 69.0);
lat_hi = latitude + (radius / 69.0);
# Build limit clause.
limit = nil
if options[:limit] or options[:offset]
options[:offset] ||= 0
limit = "#{options[:offset]},#{options[:limit]}"
end
# Generate hash.
{
:select => "*, 3956 * 2 * ASIN(SQRT(" +
"POWER(SIN((#{latitude} - #{geocoder_latitude_attr}) * " +
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
"COS(#{geocoder_latitude_attr} * PI() / 180) * " +
"POWER(SIN((#{longitude} - #{geocoder_longitude_attr}) * " +
"PI() / 180 / 2), 2) )) as distance",
:conditions => [
"#{geocoder_latitude_attr} BETWEEN ? AND ? AND " +
"#{geocoder_longitude_attr} BETWEEN ? AND ?",
lat_lo, lat_hi, lon_lo, lon_hi],
:having => "distance <= #{radius}",
:order => options[:order],
:limit => limit
}
end
##
# Name of the method that returns the search string.
# #
def geocoder_method_name def geocoder_method_name
defined?(@geocoder_method_name) ? @geocoder_method_name : :location defined?(@geocoder_method_name) ?
@geocoder_method_name : :location
end
##
# Name of the latitude attribute.
#
def geocoder_latitude_attr
defined?(@geocoder_latitude_attr) ?
@geocoder_latitude_attr : :latitude
end
##
# Name of the longitude attribute.
#
def geocoder_longitude_attr
defined?(@geocoder_longitude_attr) ?
@geocoder_longitude_attr : :longitude
end end
end end
...@@ -69,15 +117,18 @@ module Geocoder ...@@ -69,15 +117,18 @@ module Geocoder
# are defined in <tt>distance_between</tt> class method. # are defined in <tt>distance_between</tt> class method.
# #
def distance_to(lat, lon, units = :mi) def distance_to(lat, lon, units = :mi)
Geocoder.distance_between(latitude, longitude, lat, lon, :units => units) mylat = read_attribute(self.class.geocoder_latitude_attr)
mylon = read_attribute(self.class.geocoder_longitude_attr)
Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
end end
## ##
# Fetch coordinates based on the object's object's +location+. Returns an # Fetch coordinates based on the object's location. Returns an
# array <tt>[lat,lon]</tt>. # array <tt>[lat,lon]</tt>.
# #
def fetch_coordinates def fetch_coordinates
Geocoder.fetch_coordinates(send(self.class.geocoder_method_name)) location = read_attribute(self.class.geocoder_method_name)
Geocoder.fetch_coordinates(location)
end end
## ##
...@@ -86,12 +137,31 @@ module Geocoder ...@@ -86,12 +137,31 @@ module Geocoder
def fetch_and_assign_coordinates def fetch_and_assign_coordinates
returning fetch_coordinates do |c| returning fetch_coordinates do |c|
unless c.blank? unless c.blank?
self.latitude = c[0] write_attribute(self.class.geocoder_latitude_attr, c[0])
self.longitude = c[1] write_attribute(self.class.geocoder_longitude_attr, c[1])
end end
end end
end end
##
# Query Google for the coordinates of the given phrase.
# Returns array [lat,lon] if found, nil if not found or if network error.
#
def self.fetch_coordinates(query)
return nil unless doc = self.search(query)
# Make sure search found a result.
e = doc.elements['kml/Response/Status/code']
return nil unless (e and e.text == "200")
# Isolate the relevant part of the result.
place = doc.elements['kml/Response/Placemark']
# If there are multiple results, blindly use the first.
coords = place.elements['Point/coordinates'].text
coords.split(',')[0...2].reverse.map{ |i| i.to_f }
end
## ##
# Calculate the distance between two points on Earth (Haversine formula). # Calculate the distance between two points on Earth (Haversine formula).
# Takes two sets of coordinates and an options hash: # Takes two sets of coordinates and an options hash:
...@@ -126,60 +196,6 @@ module Geocoder ...@@ -126,60 +196,6 @@ module Geocoder
degrees * (Math::PI / 180) degrees * (Math::PI / 180)
end end
##
# Get options hash suitable for passing to ActiveRecord.find to get
# records within a radius (in miles) of the given point.
# Taken from excellent tutorial at:
# http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
#
# Options hash may include:
#
# +latitude+ :: name of column storing latitude data
# +longitude+ :: name of column storing longitude data
# +order+ :: column(s) for ORDER BY SQL clause
# +limit+ :: number of records to return (for LIMIT SQL clause)
# +offset+ :: number of records to skip (for LIMIT SQL clause)
#
def self.find_near_options(latitude, longitude, radius = 20, options = {})
# Set defaults/clean up arguments.
options[:latitude] ||= 'latitude'
options[:longitude] ||= 'longitude'
options[:order] ||= 'distance ASC'
radius = radius.to_i
# Constrain search to a (radius x radius) square.
factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
lon_lo = longitude - (radius / factor);
lon_hi = longitude + (radius / factor);
lat_lo = latitude - (radius / 69.0);
lat_hi = latitude + (radius / 69.0);
# Build limit clause.
limit = nil
if options[:limit] or options[:offset]
options[:offset] ||= 0
limit = "#{options[:offset]},#{options[:limit]}"
end
# Generate hash.
{
:select => "*, 3956 * 2 * ASIN(SQRT(" +
"POWER(SIN((#{latitude} - #{options[:latitude]}) * " +
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
"COS(#{options[:latitude]} * PI() / 180) * " +
"POWER(SIN((#{longitude} - #{options[:longitude]}) * " +
"PI() / 180 / 2), 2) )) as distance",
:conditions => [
"#{options[:latitude]} BETWEEN ? AND ? AND " +
"#{options[:longitude]} BETWEEN ? AND ?",
lat_lo, lat_hi, lon_lo, lon_hi],
:having => "distance <= #{radius}",
:order => options[:order],
:limit => limit
}
end
## ##
# Query Google for geographic information about the given phrase. # Query Google for geographic information about the given phrase.
# Returns the XML response as a hash. This method is not intended for # Returns the XML response as a hash. This method is not intended for
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment