diff --git a/README.rdoc b/README.rdoc index 873631ee5d530bc7ba5143a34ea73ae4344023dc..1987b505813864e182256acd1630fa14cbba028f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -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. diff --git a/init.rb b/init.rb index ddd1d1723efb4b9407f4a666aa64cd71f896a35b..729e81371307e0433c1404c1d60ae77e034a81e6 100644 --- a/init.rb +++ b/init.rb @@ -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 diff --git a/lib/geocoder.rb b/lib/geocoder.rb index 745966cdcdedea70728e55ad9e58dec6136117a4..576c1fdc51dbe33205758f2bf5e1d672ca891862 100644 --- a/lib/geocoder.rb +++ b/lib/geocoder.rb @@ -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