Skip to content

Commit

Permalink
Use geocoder gem for doing the distance based database searches
Browse files Browse the repository at this point in the history
Still using geokit gem for doing the actual geocoding as that should
hopefully still work under Rails 3. However, the geocoder gem currently
has a bug where the calculated distance is always returned in miles even
when the search units are set to kilometres. For the time being doing
the conversions back to km inline.
  • Loading branch information
mlandauer committed Jan 28, 2011
1 parent eade9ce commit 58e5ae7
Show file tree
Hide file tree
Showing 21 changed files with 880 additions and 19 deletions.
8 changes: 2 additions & 6 deletions app/controllers/applications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ def index
location_text = location.to_s
end
@description << " within #{help.meters_in_words(radius.to_i)} of #{location_text}"
# TODO: More concise form using chained scope doesn't work
# http://www.binarylogic.com/2010/01/09/using-geokit-with-searchlogic/ might provide the answer
@applications = Application.paginate :origin => [location.lat, location.lng], :within => radius.to_f / 1000,
:page => params[:page], :per_page => per_page
@applications = Application.near([location.lat, location.lng], radius.to_f / 1000, :units => :km).paginate(:page => params[:page], :per_page => per_page)
elsif params[:bottom_left_lat] && params[:bottom_left_lng] && params[:top_right_lat] && params[:top_right_lng]
lat0, lng0 = params[:bottom_left_lat].to_f, params[:bottom_left_lng].to_f
lat1, lng1 = params[:top_right_lat].to_f, params[:top_right_lng].to_f
Expand Down Expand Up @@ -86,8 +83,7 @@ def address
else
@q = location.full_address
@other_addresses = location.all[1..-1].map{|l| l.full_address}
@applications = Application.paginate :origin => [location.lat, location.lng], :within => @radius.to_f / 1000,
:page => params[:page], :per_page => per_page
@applications = Application.near([location.lat, location.lng], @radius.to_f / 1000, :units => :km).paginate(:page => params[:page], :per_page => per_page)
@rss = applications_path(:format => 'rss', :address => @q, :radius => @radius)
end
end
Expand Down
5 changes: 2 additions & 3 deletions app/controllers/layar_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ class LayarController < ApplicationController
include ActionView::Helpers::TextHelper

def getpoi
@applications = Application.paginate :origin => [params[:lat].to_f, params[:lon].to_f], :within => params[:radius].to_f / 1000,
:page => params[:pageKey], :per_page => 10
@applications = Application.near([params[:lat].to_f, params[:lon].to_f], params[:radius].to_f / 1000, :units => :km).paginate(:page => params[:pageKey], :per_page => 10)
layar_applications = @applications.map do |a|
lines = word_wrap(a.description, :line_width => 35).split("\n")
line4 = truncate(lines[2..-1].join(" "), :length => 35) if lines[2..-1]
{
:actions => [{:label => "More info", :uri => application_url(:host => Configuration::HOST, :utm_medium => 'ar', :utm_source => 'layar', :id => a.id)}],
:attribution => nil,
:distance => a.distance.to_f * 1000,
:distance => a.distance.to_f / 0.621371192 * 1000,
:id => a.id,
:imageURL => nil,
:lat => a.lat * 1000000,
Expand Down
2 changes: 1 addition & 1 deletion app/models/alert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def location

# Applications that have been scraped since the last time the user was sent an alert
def recent_applications
Application.find(:all, :origin => [location.lat, location.lng], :within => radius_km, :conditions => ['date_scraped > ?', last_sent || Date.yesterday])
Application.near([location.lat, location.lng], radius_km, :units => :km).find(:all, :conditions => ['date_scraped > ?', last_sent || Date.yesterday])
end

def radius_km
Expand Down
4 changes: 2 additions & 2 deletions app/models/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Application < ActiveRecord::Base
belongs_to :authority
has_many :comments
before_save :geocode
acts_as_mappable :default_units => :kms
geocoded_by :address, :latitude => :lat, :longitude => :lng

default_scope :order => "date_scraped DESC"

Expand Down Expand Up @@ -126,7 +126,7 @@ def fourd_distance_squared(scaled_value)
def find_all_nearest_or_recent(max_distance = 2, max_age = 2 * 4 * 7 * 24 * 60 * 60)
if location
# TODO: Do the sort with SQL so that we can limit the data transferred
apps = Application.find(:all, :origin => [location.lat, location.lng], :within => max_distance, :conditions => ['date_scraped > ?', max_age.seconds.ago])
apps = Application.near([location.lat, location.lng], max_distance, :units => :km).find(:all, :conditions => ['date_scraped > ?', max_age.seconds.ago])

now = Time.now
ratio = max_distance / max_age
Expand Down
2 changes: 1 addition & 1 deletion app/views/applications/_application.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
%dd
.appinfo
- if application.respond_to? :distance
#{time_ago_in_words(application.date_scraped)} ago, #{km_in_words(application.distance.to_f)} away
#{time_ago_in_words(application.date_scraped)} ago, #{km_in_words(application.distance.to_f / 0.621371192)} away
- else
#{time_ago_in_words(application.date_scraped)} ago
.description
Expand Down
2 changes: 1 addition & 1 deletion app/views/applications/_application_mobile.haml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
%h4= application_mobile.address
%p
- if application_mobile.respond_to? :distance
#{time_ago_in_words(application_mobile.date_scraped)} ago, #{km_in_words(application_mobile.distance.to_f)} away
#{time_ago_in_words(application_mobile.date_scraped)} ago, #{km_in_words(application_mobile.distance.to_f / 0.621371192)} away
- else
#{time_ago_in_words(application_mobile.date_scraped)} ago
%strong= application_mobile.description
6 changes: 3 additions & 3 deletions spec/controllers/applications_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
@result = mock

Location.should_receive(:geocode).with("24 Bruce Road Glenbrook").and_return(location)
Application.should_receive(:paginate).with(:origin => [location.lat, location.lng], :within => 4, :page => nil, :per_page => 100).and_return(@result)
Application.should_receive(:paginate).with(:page => nil, :per_page => 100).and_return(@result)
end

it "should find recent applications near the address" do
Expand All @@ -61,7 +61,7 @@
result = mock

Location.should_receive(:geocode).with("24 Bruce Road Glenbrook").and_return(location)
Application.should_receive(:paginate).with(:origin => [location.lat, location.lng], :within => 2, :page => nil, :per_page => 100).and_return(result)
Application.should_receive(:paginate).with(:page => nil, :per_page => 100).and_return(result)

get :index, :address => "24 Bruce Road Glenbrook"
assigns[:applications].should == result
Expand All @@ -73,7 +73,7 @@
before :each do
@result = mock

Application.should_receive(:paginate).with(:origin => [1.0, 2.0], :within => 4, :page => nil, :per_page => 100).and_return(@result)
Application.should_receive(:paginate).with(:page => nil, :per_page => 100).and_return(@result)
end

it "should find recent applications near the point" do
Expand Down
5 changes: 3 additions & 2 deletions spec/controllers/layar_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

describe LayarController do
it "should provide a rest api to serve the layar points of interest" do
application = mock(:distance => 2, :lat => 1.0, :lng => 2.0, :id => 101, :address => " 1 Foo St\n Fooville",
# TODO Silly bug in "geocoder" gem means that distances are returned in miles even when the units are set to kilometres
application = mock(:distance => 2 * 0.621371192, :lat => 1.0, :lng => 2.0, :id => 101, :address => " 1 Foo St\n Fooville",
:description => "1234 678901234 67890123 56789 12345 1234567 90123456789 123456 89\n1234567890 2345678 012345678 01234512345")
result = [application]
result.stub!(:current_page).and_return(1)
result.stub!(:total_pages).and_return(2)
Application.should_receive(:paginate).with(:origin => [1.0, 2.0], :within => 3.0, :page => "2", :per_page => 10).and_return(result)
Application.should_receive(:paginate).with(:page => "2", :per_page => 10).and_return(result)

get :getpoi, :lat => 1.0, :lon => 2.0, :radius => 3000, :pageKey => "2"
assigns[:applications].should == result
Expand Down
5 changes: 5 additions & 0 deletions vendor/plugins/geocoder/.document
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE
77 changes: 77 additions & 0 deletions vendor/plugins/geocoder/CHANGELOG.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
= Changelog

Per-release changes to Geocoder.

== 0.9.5 (TBA)

* Fix broken PostgreSQL compatibility (now 100% compatible).
* Switch from Google's XML to JSON geocoding API.
* Separate Rails 2 and Rails 3-compatible branches.
* Don't allow :conditions hash in 'options' argument to 'nearbys' method (was deprecated in 0.9.3).

== 0.9.4 (2010 Aug 2)

* Google Maps API key no longer required (uses geocoder v3).

== 0.9.3 (2010 Aug 2)

* Fix incompatibility with Rails 3 RC 1.
* Deprecate 'options' argument to 'nearbys' method.
* Allow inclusion of 'nearbys' in Arel method chains.

== 0.9.2 (2010 Jun 3)

* Fix LIMIT clause bug in PostgreSQL (reported by kenzie).

== 0.9.1 (2010 May 4)

* Use scope instead of named_scope in Rails 3.

== 0.9.0 (2010 Apr 2)

* Fix bug in PostgreSQL support (caused "PGError: ERROR: column "distance" does not exist"), reported by developish.

== 0.8.9 (2010 Feb 11)

* Add Rails 3 compatibility.
* Avoid querying Google when query would be an empty string.

== 0.8.8 (2009 Dec 7)

* Automatically select a less accurate but compatible distance algorithm when SQLite database detected (fixes SQLite incompatibility).

== 0.8.7 (2009 Nov 4)

* Added Geocoder.geographic_center method.
* Replaced _get_coordinates class method with read_coordinates instance method.

== 0.8.6 (2009 Oct 27)

* The fetch_coordinates method now assigns coordinates to attributes (behaves like fetch_coordinates! used to) and fetch_coordinates! both assigns and saves the attributes.
* Added geocode:all rake task.

== 0.8.5 (2009 Oct 26)

* Avoid calling deprecated method from within Geocoder itself.

== 0.8.4 (2009 Oct 23)

* Deprecate <tt>find_near</tt> class method in favor of +near+ named scope.

== 0.8.3 (2009 Oct 23)

* Update Google URL query string parameter to reflect recent changes in Google's API.

== 0.8.2 (2009 Oct 12)

* Allow a model's geocoder search string method to be something other than an ActiveRecord attribute.
* Clean up documentation.

== 0.8.1 (2009 Oct 8)

* Extract XML-fetching code from Geocoder.search and place in Geocoder._fetch_xml (for ease of mocking).
* Add tests for coordinate-fetching instance methods.

== 0.8.0 (2009 Oct 1)

First release.
20 changes: 20 additions & 0 deletions vendor/plugins/geocoder/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2009-10 Alex Reisner

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
122 changes: 122 additions & 0 deletions vendor/plugins/geocoder/README.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
= Geocoder

Geocoder adds object geocoding and database-agnostic distance calculations to Ruby on Rails. It's as simple as calling <tt>fetch_coordinates!</tt> on your objects, and then using a scope like <tt>Venue.near("Billings, MT")</tt>. Since it does not rely on proprietary database functions finding geocoded objects in a given area works with out-of-the-box MySQL or even SQLite.

Geocoder is currently compatible with Rails 2.x and 3.x. <b>This is the README for the 2.x branch.</b> Please see the 3.x branch for the lastest installation instructions, documentation, and issues.


== 1. Install

Install as a plugin:

script/plugin install git://github.com/alexreisner/geocoder.git -r rails2


== 2. Configure

A) Add +latitude+ and +longitude+ columns to your model:

script/generate migration AddLatitudeAndLongitudeToYourModel latitude:float longitude:float
rake db:migrate

B) Tell geocoder where your model stores its address:

geocoded_by :address

C) Optionally, auto-fetch coordinates every time your model is saved:

after_validation :fetch_coordinates

<i>Note that you are not stuck with the +latitude+ and +longitude+ column names, or the +address+ method. See "More On Configuration" below for details.</i>


== 3. Use

Assuming +obj+ is an instance of a geocoded class, you can get its coordinates:

obj.fetch_coordinates # fetches and assigns coordinates
obj.fetch_coordinates! # also saves lat, lon attributes

If you have a lot of objects you can use this Rake task to geocode them all:

rake geocode:all CLASS=YourModel

Once +obj+ is geocoded you can do things like this:

obj.nearbys(30) # other objects within 30 miles
obj.distance_to(40.714, -100.234) # distance to arbitrary point

To find objects by location, use the following scopes:

Venue.near('Omaha, NE, US', 20) # venues within 20 miles of Omaha
Venue.near([40.71, 100.23], 20) # venues within 20 miles of a point
Venue.geocoded # venues with coordinates
Venue.not_geocoded # venues without coordinates

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")

# find the geographic center (aka center of gravity) of objects or points
Geocoder.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])


== More On Configuration

You are not stuck with using the +latitude+ and +longitude+ database column names for storing coordinates. For example, to use +lat+ and +lon+:

geocoded_by :address, :latitude => :lat, :longitude => :lon

The string to use for geocoding can be anything you'd use to search Google Maps. For example, 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 you might do something like this:

geocoded_by :location

def location
[address, city, state, country].compact.join(', ')
end


Please see the code for more methods and detailed information about arguments (eg, working with kilometers).


== SQLite

SQLite's lack of trigonometric functions requires an alternate implementation of the +near+ method (scope). When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius.

It is also not possible to calculate distances between points without the trig functions so you cannot sort results by "nearness."


=== Discussion

There are few options for finding objects near a given point in SQLite without installing extensions:

1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius--at worst 29% farther away), but you will get all the points within the required radius.

2. Load all objects into memory and compute distances between them using the <tt>Geocoder.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.

3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder.distance_between</tt>.

Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.


== Known Issue

You cannot use the +near+ scope with another scope that provides an +includes+ option because the +SELECT+ clause generated by +near+ will overwrite it (or vice versa). Instead, try using +joins+ and pass a <tt>:select</tt> option to the +near+ scope to get the columns you want. For example, in Rails 2 syntax:

# instead of :includes => :venues:
City.near("Omaha, NE", 20, :select => "venues.*").all(:joins => :venues)

If anyone has a more elegant solution to this problem I am very interested in seeing it.


Copyright (c) 2009-10 Alex Reisner, released under the MIT license
Loading

0 comments on commit 58e5ae7

Please sign in to comment.