In-memory and in-database distance calculations

Feb 11, 2007 by Andre

Following up on our recent release of GeoKit, Bill and I are working on some posts highlighting some of GeoKit's cool features. This post is on distance calculations. GeoKit can calculate distance in memory, and it can calculate distance as part of a database query.

In-memory distance calculation

The basic syntax for in-memory distance calculations is: d = first_loc.distance_to(second_loc)

What class is first_loc and second_loc? They should be either LatLng (Geokit::LatLng), or GeoLoc. If you open up the mappable.rb file, you're see that GeoLoc inherits from LatLng. If you're looking for the distance_to method, you'll find it in the Mappable module (the code for which is also in mappable.rb). The Mappable module is mixed in to LatLng, and imbues it with the distance_to method.

Distance in miles and kilometers

By default, distance_to yields a result in miles. If you want a distance in kilometers instead, just pass the optional units argument:

>>d = first_loc.distance_to(second_loc, :units=>:kilos)

A full example

Here we are just instantiating two LatLng objects using raw latitudes and longitudes (which correspond to San Francisco, CA, and Irving, TX).
>ruby script/console
Loading development environment. >> include GeoKit >> first_loc=LatLng.new(37.775,-122.418) >> second_loc=LatLng.new(32.813,-96.948) >> first_loc.distance_to(second_loc) => 1473.29189341352

. . . and with geocoding

Here we are starting with just the addresses, geocoding them through Google's geocoding service, and then calculating the distance. Calls to any of the Geocoders in GeoKit return the GeoLoc object. Recall that GeoLoc inherits from LatLng though, so it has all of LatLngs methods, including distance_to.

>>ruby script/console Loading development environment. 
>> include GeoKit::Geocoders 
>> sf=GoogleGeocoder.geocode('San Francisco,CA') 
>> puts sf.ll 
37.775,-122.418333 
>> irving=GoogleGeocoder.geocode('Irving,TX') 
>> puts irving.ll 
32.813889,-96.948611 
>> sf.distance_to(irving) 
=> 1473.25508451219 
>> sf.distance_to(irving, :units=>:kilometers)
=> 2370.46743098012

Flat vs. Spherical distances

If it turns out the earth is actually flat, GeoLoc still has us covered:

sf.distance_to(irving, :formula=>:flat)
=> 1206.13692230874

In case you're wondering, the spherical formula uses the Haversine method, and flat formula just uses the pythagorian therorum. You can find both implementations in the Mappable module in mappable.rb.

So there you have in-memory distance calculations provided by Geokit. Next, let's look at distance calculations in your database through ActiveRecord finder extensions provided by ActsAsMappable.

Database Distance Calculations

I have a model named Shop, which mixes in the ActsAsMappable module:

class Shop < ActiveRecord::Base 
acts_as_mappable 
... end 
Let's find the 10 closest shops to the center of the '94117' zipcode:
>> loc=GoogleGeocoder.geocode('94117')
>> puts loc.full_address
"San Francisco, CA 94117, USA" >> shops=Shop.find :all, :origin=>loc, :order=>'distance asc', :limit=>10

Now, each of the shops that has a bonus attribute, 'distance'. This represents the distance from the origin provided in the query.

>> puts shops[0].distance
0.22292201411219
>> puts shops[9].distance
0.61426029124168
>> shops.each {|shop| puts "#{shop.name} -- #{shop.distance} miles from #{loc.full_address}"}
Coffee to the People -- 0.22292201411219 miles from San Francisco, CA 94117, USA
Central Coffee, Tea & Spice -- 0.24258501247455 miles from San Francisco, CA 94117, USA
Bean Bag Cafe -- 0.33106086315379 miles from San Francisco, CA 94117, USA
Cafe Abir -- 0.42131954288221 miles from San Francisco, CA 94117, USA
Cole Valley Cafe -- 0.53454381452665 miles from San Francisco, CA 94117, USA
Jumping Java -- 0.5419605766772 miles from San Francisco, CA 94117, USA
Cafe Reverie -- 0.59076205971555 miles from San Francisco, CA 94117, USA
Cafe Du Soleil -- 0.60029433169774 miles from San Francisco, CA 94117, USA
Tully's (Cole Valley) -- 0.61426029124168 miles from San Francisco, CA 94117, USA

Since distance is being calculated in the database, you can use it in the conditions like any other column:

>> shops=Shop.find :all, :origin=>loc, :order=>'distance asc', 
                         :conditions=>'distance>20 AND distance <25'

Database and memory distance calculations together

Since my Shop class mixes in the Mappable module (through the acts_as_mappable call), I can also calculate its distance from any arbitrary location:

shops=Shop.find :all, :origin=>loc, :order=>'distance asc',
                         :conditions=>'distance>20 AND distance <25'
>> other_loc=GoogleGeocoder.geocode('100 Spear St, San Francisco, CA')
>> puts shops[0].distance_to(other_loc) # distance to an arbitrary location
=> 19.6377479942219
>> puts shops[0].distance # distance to the origin given in the query
22.581680094216

In case you're wondering, the math happening behind the scenes is essentially the same as outlined in sorting your queries by geographic distance -- they math is now nicely hidden, thanks to Bill Eisenhaurer's work in abstracting it away.

Summary

GeoKit supports both in-memory and in-database distance calculations. You can use units of miles or kilometers, and a flat or spherical formula.

Coming up next, I’m going to talk more about the find’s :origin argument.

Bill is also putting up some more GeoKit documentation and examples -- check out his live example of IP-based geocoding. It’s very cool stuff which allows you to determine roughly where your visitors are based on their IP address. GeoKit makes it easy for any Rails controller to employ the functionality.

Comments

1

Bill Eisenhauer on Feb 11

Awesome post, Andre! Hopefully, my live examples will be done soon as well.

2

Dan on Feb 12

Would you consider making your distance_to method so that the first argument can also be a String (in addition to a GeoLoc object), and if it is, you use MultiGeocoder to geocode it? So instead of code like:

other_loc = GoogleGeocoder.geocode('100 Spear St, San Francisco, CA')
puts shops[0].distance_to(other_loc)

You can just do:

puts shops[0].distance_to('100 Spear St, San Francisco, CA')

Its a small suggestion, but I didn't see the point in instantiating objects that I have no intention of using afterwards.

3

Andre Lewis on Feb 12

Dan, thanks for the suggestion. The thing is, the GeoLoc object will get created internally in the geocoders anyway (unless we make internal changes to the geocoders, which I don't want to do right now). For now use the form:
shops[0].distance_to(MultiGeocoder.geocode('100 spear st'))

Keep in mind that geocoding can fail -- unless you're very sure of the integrity of the incoming address, you'll want to check the success of the geocoding operation before utilizing the result.

4

Bill Eisenhauer on Feb 14

Hey Dan,

We added your suggestion. Take a look here:

blog.billeisenhauer.com/2007/02/14/geokit-improvements-to-distance_between-and-distance_to/

5

Doris on Jun 14

If you are looking for latitude and longitude data for your application, you can receive up-to-date geo data for N. American Zipcodes, along with latitude and longitude, county, and country information from www.meridianworlddata.com. You can also download complete geographical databases for cities, land features, water features, and man made features around the world. You can then use this data within your calculations to make sure you have the most up-to-date data.

6

carlivar on Jul 30

The bummer is that :include doesn't work.

What if you want to find all shops nearby that have a certain product in stock? Normally you'd just find(:origin => @origin, :include => :product with :conditions => ['products.id = ?' @product_searching_for.id])

This doesn't work. :(

jystewart.net/process/2007/03/select-and-include-in-activerecord-queries/

7

Andre Lewis on Jul 30

Carl, the above use case does work now. You need the update to GeoKit I made earlier this month.

Specifically, you can include a has_many relationship, specify a condition on the included table, AND sort by distance from an origin.

One caveat is that while the distance sorting works in the database, the distance column itself is not returned in the result set. You can add it back in post-query with:

result.sort_by_distance_from(origin)

Cheers,

Andre

8

Sebastian on May 06

This is just fantastic!
Thank you very much!

9

Oliver on Jun 25

Distance calculations are great, I'm having a problem with sorting the results from a database calculation though, I want to get back the closest 5 places but the order isn't doing what it should be, this is what I've got -

Station.find(:all, :origin =>location, :order =>"distance", :limit => 5)

It is entirely possible that I'm doing something dumb but I really can't see it.

10

Rustam on Sep 18

Would you consider extending your database finders to work with in-memory calculations, so you could just do this: Events.find_all.find(:origin=>some_origin, :distance=>some_distance)?

11

Jack on Mar 06

First, thanks for bringing us such an awesome book - Beginning Google Maps Applications with Rails and Ajax.

Second, at the risk of asking a dumb question, which approach - in-memory or in-database - should yield best performance on average?

Appreciate your help!

Post a comment

Stay in Touch

spacer
spacer
Recommend me at WWR -->

Last Five Posts

Rails on Ruby 1.9.1 in production: just do it
RubyMine + Snow Leopard
Business lessons learned
Load Averages, Explained
git remote branch

My Book

Available now from Apress

Categories

  • Ajax
  • Entrepreneurial
  • Gadgets
  • GeoKit
  • Google
  • Google Maps
  • How-to
  • Javascript
    • Prototype
    • jQuery
  • Misc
  • MySQL
  • Open source
  • OpenID
  • PlaceShout
  • Railsconf
  • Ruby
  • Ruby on Rails
  • SEO
  • Scout
  • ShapeWiki
  • Sinatra
  • Speaking / writing
  • Tools
    • JSLog
  • Usability
  • Web
  • Web 2.0
  • Wifi Cafes
  • git
  • iphone
  • system

My WiFi site

Top cities: San Francisco WiFi; Portland WiFi; Charlotte WiFi

 
This is so filters can reject the spam-bots. Thanks!
 
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.