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

Merge branch 'master' of git@github.com:alexreisner/geocoder

Conflicts:
	lib/geocoder.rb
parents a685735d 2efda569
No related branches found
No related tags found
No related merge requests found
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE
*.sw?
.DS_Store
coverage
rdoc
pkg
Copyright (c) 2009 [name of plugin creator]
Copyright (c) 2009 Alex Reisner
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
......
= Geocoder
Geocoder is a simple plugin for Rails that provides object geocoding (via
Google Maps) and some utilities for working with geocoded objects. The code can
be used as a standalone method provider or included in a class to give objects
geographic awareness.
Geocoder adds database-agnostic object geocoding to Rails (via Google). It does not rely on proprietary database functions so reasonably accurate distances can be calculated in MySQL or even SQLite.
== Setup
== Install
Use the Rails plugin install script:
Install either as a plugin:
script/plugin install git://github.com/alexreisner/geocoder.git
or as a gem:
# add to config/environment.rb:
config.gem "rails-geocoder", :lib => "geocoder", :source => "http://gemcutter.org/"
# at command prompt:
sudo rake gems:install
== Configure
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, to use +address+, +lat+, and +lon+ respectively:
geocoded_by :address, :latitude => :lat, :longitude => :lon
A geocodable string is 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:
If your model has +address+, +city+, +state+, and +country+ attributes your +location+ method might look something like this:
def location
[address, city, state, country].compact.join(', ')
end
== Use
== Examples
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.find_near([40.71, 100.23], 20) # venues within 20 miles of a point
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+:
Find distance between object and a point:
obj.fetch_coordinates # returns coordinates [lat, lon]
obj.fetch_coordinates! # also writes coordinates to object
obj.distance_to(40.71432, -100.23487) # in miles
obj.distance_to(40.71432, -100.23487, :km) # in kilometers
Assuming +obj+ is geocoded (has latitude and longitude):
Find objects within 20 miles of a point:
obj.nearbys(30) # other objects within 30 miles
obj.distance_to(40.714, -100.234) # distance to arbitrary point
Venue.near('Omaha, NE, US', 20) # Venue is a geocoded model
Please see the code for more methods and detailed information about arguments.
Some utility methods are also available:
# 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")
== To-do
* +near+ method should also accept an array of coordinates as its first parameter
Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
Copyright (c) 2009 Alex Reisner, released under the MIT license
require 'rubygems'
require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "rails-geocoder"
gem.summary = %Q{Add geocoding functionality to Rails models.}
gem.description = %Q{Add geocoding functionality to Rails models.}
gem.email = "alex@alexreisner.com"
gem.homepage = "http://github.com/alexreisner/geocoder"
gem.authors = ["Alex Reisner"]
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
end
require 'rake/testtask'
require 'rake/rdoctask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |test|
test.libs << 'test'
test.pattern = 'test/**/*_test.rb'
test.verbose = true
end
rescue LoadError
task :rcov do
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
end
end
task :test => :check_dependencies
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the geocoder plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
if File.exist?('VERSION')
version = File.read('VERSION')
else
version = ""
end
desc 'Generate documentation for the geocoder plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Geocoder'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.title = "geocoder #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
0.8.0
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)
include Geocoder
@geocoder_method_name = method_name
end
end
require 'geocoder'
\ No newline at end of file
......@@ -12,33 +12,16 @@ module Geocoder
# named scope: geocoded objects
named_scope :geocoded,
:conditions => "latitude IS NOT NULL AND longitude IS NOT NULL"
:conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
"AND #{geocoder_options[:longitude]} IS NOT NULL"
# named scope: not-geocoded objects
named_scope :not_geocoded,
:conditions => "latitude IS NULL OR longitude IS NULL"
:conditions => "#{geocoder_options[:latitude]} IS NULL " +
"OR #{geocoder_options[:longitude]} IS NULL"
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.
#
......@@ -46,61 +29,145 @@ module Geocoder
##
# Find all objects within a radius (in miles) of the given location
# (address string).
# (address string). Location (the first argument) may be either a string
# to geocode or an array of coordinates (<tt>[lat,long]</tt>).
#
def near(location, radius = 100, options = {})
latitude, longitude = Geocoder.fetch_coordinates(location)
def find_near(location, radius = 20, options = {})
latitude, longitude = location.is_a?(Array) ?
location : Geocoder.fetch_coordinates(location)
return [] unless (latitude and longitude)
query = nearby_mysql_query(latitude, longitude, radius.to_i, options)
find_by_sql(query)
all(find_near_options(latitude, longitude, radius, options))
end
##
# Generate a MySQL query to find all records within a radius (in miles)
# of a point.
# 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 nearby_mysql_query(latitude, longitude, radius = 20, options = {})
table = options[:table_name] || self.to_s.tableize
options.delete :table_name # don't pass to nearby_mysql_query
Geocoder.nearby_mysql_query(table, latitude, longitude, radius, options)
end
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
lat_attr = geocoder_options[:latitude]
lon_attr = geocoder_options[:longitude]
{
:select => "*, 3956 * 2 * ASIN(SQRT(" +
"POWER(SIN((#{latitude} - #{lat_attr}) * " +
"PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
"COS(#{lat_attr} * PI() / 180) * " +
"POWER(SIN((#{longitude} - #{lon_attr}) * " +
"PI() / 180 / 2), 2) )) as distance",
:conditions => [
"#{lat_attr} BETWEEN ? AND ? AND " +
"#{lon_attr} BETWEEN ? AND ?",
lat_lo, lat_hi, lon_lo, lon_hi],
:having => "distance <= #{radius}",
:order => options[:order],
:limit => limit
}
end
##
# Get the name of the method that returns the search string.
# Get the coordinates [lat,lon] of an object. This is not great but it
# seems cleaner than polluting the object method namespace.
#
def geocoder_method_name
defined?(@geocoder_method_name) ? @geocoder_method_name : :location
def _get_coordinates(object)
[object.send(geocoder_options[:latitude]),
object.send(geocoder_options[:longitude])]
end
end
##
# Is this object geocoded? (Does it have latitude and longitude?)
#
def geocoded?
self.class._get_coordinates(self).compact.size > 0
end
##
# Calculate the distance from the object to a point (lat,lon). Valid units
# 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)
return nil unless geocoded?
mylat,mylon = self.class._get_coordinates(self)
Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
end
##
# Fetch coordinates based on the object's object's +location+. Returns an
# array <tt>[lat,lon]</tt>.
# Get other geocoded objects within a given radius.
# The object must be geocoded before this method is called.
#
def nearbys(radius = 20)
return [] unless geocoded?
lat,lon = self.class._get_coordinates(self)
self.class.find_near([lat, lon], radius) - [self]
end
##
# 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_options[:method_name])
Geocoder.fetch_coordinates(location)
end
##
# Fetch and assign +latitude+ and +longitude+.
# Fetch coordinates and assign +latitude+ and +longitude+.
#
def fetch_and_assign_coordinates
def fetch_coordinates!
returning fetch_coordinates do |c|
unless c.blank?
self.latitude = c[0]
self.longitude = c[1]
write_attribute(self.class.geocoder_options[:latitude], c[0])
write_attribute(self.class.geocoder_options[:longitude], 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:
......@@ -108,19 +175,23 @@ module Geocoder
# +units+ :: <tt>:mi</tt> for miles (default), <tt>:km</tt> for kilometers
#
def self.distance_between(lat1, lon1, lat2, lon2, options = {})
# set default options
options[:units] ||= :mi
# define available units
# define conversion factors
units = { :mi => 3956, :km => 6371 }
# convert degrees to radians
lat1 *= Math::PI / 180
lon1 *= Math::PI / 180
lat2 *= Math::PI / 180
lon2 *= Math::PI / 180
lat1 = to_radians(lat1)
lon1 = to_radians(lon1)
lat2 = to_radians(lat2)
lon2 = to_radians(lon2)
# compute distances
dlat = (lat1 - lat2).abs
dlon = (lon1 - lon2).abs
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
......@@ -128,52 +199,12 @@ module Geocoder
end
##
# Find all 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)
# Convert degrees to radians.
#
def self.nearby_mysql_query(table, latitude, longitude, radius = 20, options = {})
# Alternate column names.
options[:latitude] ||= 'latitude'
options[:longitude] ||= 'longitude'
options[:order] ||= 'distance ASC'
# 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);
where = "#{options[:latitude]} BETWEEN #{lat_lo} AND #{lat_hi} AND " +
"#{options[:longitude]} BETWEEN #{lon_lo} AND #{lon_hi}"
# Build limit clause.
limit = ""
if options[:limit] or options[:offset]
options[:offset] ||= 0
limit = "LIMIT #{options[:offset]},#{options[:limit]}"
end
# Generate query.
"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 " +
"FROM #{table} WHERE #{where} HAVING distance <= #{radius} " +
"ORDER BY #{options[:order]} #{limit}"
def self.to_radians(degrees)
degrees * (Math::PI / 180)
end
##
# Query Google for geographic information about the given phrase.
# Returns the XML response as a hash. This method is not intended for
......@@ -202,3 +233,22 @@ module Geocoder
REXML::Document.new(doc)
end
end
##
# Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
#
ActiveRecord::Base.class_eval do
##
# Set attribute names and include the Geocoder module.
#
def self.geocoded_by(method_name = :location, options = {})
class_inheritable_reader :geocoder_options
write_inheritable_attribute :geocoder_options, {
:method_name => method_name,
:latitude => options[:latitude] || :latitude,
:longitude => options[:longitude] || :longitude
}
include Geocoder
end
end
# Generated by jeweler
# DO NOT EDIT THIS FILE
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{rails-geocoder}
s.version = "0.8.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Alex Reisner"]
s.date = %q{2009-10-01}
s.description = %q{Add geocoding functionality to Rails models.}
s.email = %q{alex@alexreisner.com}
s.extra_rdoc_files = [
"LICENSE",
"README.rdoc"
]
s.files = [
".document",
".gitignore",
"LICENSE",
"README.rdoc",
"Rakefile",
"VERSION",
"init.rb",
"lib/geocoder.rb",
"rails-geocoder.gemspec",
"test/geocoder_test.rb",
"test/test_helper.rb"
]
s.homepage = %q{http://github.com/alexreisner/geocoder}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.5}
s.summary = %q{Add geocoding functionality to Rails models.}
s.test_files = [
"test/geocoder_test.rb",
"test/test_helper.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else
end
else
end
end
# desc "Explaining what the task does"
# task :geocoder do
# # Task goes here
# end
require 'test/unit'
require 'test_helper'
class GeocoderTest < Test::Unit::TestCase
# Replace this with your real tests.
......
require 'rubygems'
require 'test/unit'
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'geocoder'
class Test::Unit::TestCase
end
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