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:
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
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:
......@@ -23,21 +31,32 @@ If your model has +address+, +city+, +state+, and +country+ attributes your +loc
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]
obj.fetch_and_assign_coordinates # writes values to +latitude+ and +longitude+
Assuming +obj+ has a valid string for its +location+:
obj.fetch_coordinates # returns coordinates [lat, lon]
obj.fetch_and_assign_coordinates # writes values to object
Find distance between object and a point:
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) # in miles
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.
......
......@@ -4,8 +4,10 @@ ActiveRecord::Base.class_eval do
# Include the Geocoder module and set the method name which returns
# the geo-search string.
#
def self.geocoded_by(method_name = :location)
def self.geocoded_by(method_name = :location, options = {})
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
......@@ -20,25 +20,6 @@ module Geocoder
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.
#
......@@ -53,14 +34,81 @@ module Geocoder
latitude, longitude = location.is_a?(Array) ?
location : Geocoder.fetch_coordinates(location)
return [] unless (latitude and longitude)
all(Geocoder.find_near_options(latitude, longitude, radius, options))
all(find_near_options(latitude, longitude, radius, options))
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
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
......@@ -69,15 +117,18 @@ module Geocoder
# are defined in <tt>distance_between</tt> class method.
#
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
##
# 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>.
#
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
##
......@@ -86,12 +137,31 @@ module Geocoder
def fetch_and_assign_coordinates
returning fetch_coordinates do |c|
unless c.blank?
self.latitude = c[0]
self.longitude = c[1]
write_attribute(self.class.geocoder_latitude_attr, c[0])
write_attribute(self.class.geocoder_longitude_attr, c[1])
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).
# Takes two sets of coordinates and an options hash:
......@@ -126,60 +196,6 @@ module Geocoder
degrees * (Math::PI / 180)
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.
# 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