Main

New GeoKit Release

Jul 22 by Andre

I committed some major improvements to GeoKit recently. Head over to GeoKit at Rubyforge for an updated readme and API. Here is a rundown of the improvements. As always, install with:

sudo script/plugin install svn://rubyforge.org/var/svn/geokit/trunk

New Functionality

  • auto geocoding: an option to automatically geocode a model's address field on create
  • in-memory sort-by-distance for arrays of location objects
  • bounding box queries: Location.find :all, :bounds=>[sw,ne]
  • improved performance by automatically adding a bounding box condition to radial queries
  • new Bounds class for in-memory bounds-related operations
  • ability to calculate heading and midpoint between two points
  • ability to calculate endpoint given a point, heading, and distance

Auto Geocoding

If your geocoding needs are simple, you can tell your model to automatically geocode itself on create:

class Store < ActiveRecord::Base
  acts_as_mappable :auto_geocode=>true
end

It takes two optional params:

class Store < ActiveRecord::Base
  acts_as_mappable :auto_geocode=>{:field=>:address, :error_message=>'Could not geocode address'}
end

. . . which is equivalent to:

class Store << ActiveRecord::Base
  acts_as_mappable
  before_validation_on_create :geocode_address

  private
  def geocode_address
    geo=GeoKit::Geocoders::MultiGeocoder.geocode (address)
    errors.add(:address, "Could not Geocode address") if !geo.success
    self.lat, self.lng = geo.lat,geo.lng if geo.success
  end
end

If you need any more complicated geocoding behavior for your model, you should roll your own before_validate callback.

In-memory sort-by-distance for arrays of location objects

Usually, you can do your sorting in the database as part of your find call. If you need to sort things post-query, you can do so:

stores=Store.find :all
stores.sort_by_distance_from(home)
puts stores.first.distance

Obviously, each of the items in the array must have a latitude/longitude so they can be sorted by distance.

You may need to do this when you use :include. You can use includes along with your distance finders:

stores=Store.find :all, :origin=>home, :include=>[:reviews,:cities] :within=>5, :order=>'distance'

However, ActiveRecord drops the calculated distance column when you use include. So, if you need to use the distance column, you'll have to re-calculate it post-query in Ruby:

stores.sort_by_distance_from(home)

The in-memory distance calcs are probably slower than the DB, but at least you can get them if you really need to. It's certainly tolerable on a small result set.

Bounding boxes

There is a new class, GeoKit::Bounds

bounds=GeoKit::Bounds.new(sq_sw_point,ne_point)

or, if you need to created it from a point and radius:

bounds=GeoKit::Bounds.from_point_and_radius(home,5)

If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map:

Shop.find :all, :bounds=>[sw_point,ne_point]

or, if you already have a Bounds instance:

Shop.find :all, :bounds=>bounds

The input to :bounds can be array with the two points or a Bounds object. However you provide them, the order should always be the southwest corner, northeast corner of the rectangle. Typically, you will be getting the swpoint and nepoint from a map that is displayed on a web page.

You can also find the center of a box, and determine if an individual point is inside a box. See the API for details.

Performance Improvement w/radial queries

When you do a radial query (Shop.find :all, :origin=>home, :within=>5), GeoKit will automatically create a bounding box around the circle to improve performance. It will only apply the bounding box if you don't provide a bounding box yourself.

Distances, headings, endpoints, and midpoints

distance=home.distance_from(work, :units=>:miles)
heading=home.heading_to(work) # result is in degrees, 0 is north
endpoint=home.endpoint(90,2)  # two miles due east
midpoing=home.midpoint_to(work)

Bug Fixes

1) fixed problem when :including another model which also has lat lat/lng columns. Previously, a query like this wouldn't work.

2) added logic to support :include together with :order. Whenever you use :include, ActiveRecord drops the 'distance' pseudo-column from the select, which means that the :order clause couldn't reference it. I added logic to replace the reference to the literal "distance" column in the order clause with the distance_sql only when you are also using an :include. This keeps it from bombing when you use :include and :order together.

Continue reading "New GeoKit Release

Using GeoKit? Tell us about it

Mar 13 by Andre

If you are using GeoKit, drop me an email. I am going to put up a "Who's using GeoKit?" page at geokit.rubyforge.org, and I'd love to hear:

  • how you're using GeoKit
  • what site or service you're using it on, with a link that I can put up on Rubyforge
  • how GeoKit made your life (or at least your project!) easier

Send me an email at andre {at} earthcode {dot} com. Thanks!

Continue reading "Using GeoKit? Tell us about it

GeoKit: all about the :origin argument

Feb 15 by Andre

The whole point of the acts_as_mappable mixin is to let you find things by location:

class Store < ActiveRecord::Base
    acts_as_mappable
    ...
end

...

Store.find(:all, :origin=>[37.792,-122.393])

But, what all can be supplied in the :origin argument? There are three possibilities.

1. Pass in a two-element array with latitudes/longitude values

Store.find(:all, :origin=>[37.792,-122.393])

Whenever :origin is an array, the code assumes in an array of two numbers representing latitude and longitude. If you pass in an array of anything else, you'll get an error.

2. Pass in a geocodeable string

Store.find(:all, :origin=>'100 Spear st, San Francisco, CA')

Again, the detection is based on the type of thing passed in under origin. So, if you expect GeoKit to geocode it, it better be in the format of a string. Zip codes are fine, but it better be a string. So, this will work:

Store.find(:all, :origin=>'94111') # it's a string, so it gets geocoded

But this will not:

# WON'T WORK -- it's not a string, so it doesn't get geocoded
Store.find(:all, :origin=>94111)

3. pass an object

Specifically, an object that has lat and lng methods, or latitude and longitude methods, or whatever methods you have specified for lng_column_name and lat_column_name.

Store.find(:all, :origin=>an_object_with_the_right_methods)

So, what sort of objects have lat and lng methods? Well, our own GeoKit::LatLng objects for starters. So, this will certainly work:

ll=LatLng.new(37.792,-122.393])
Store.find(:all, :origin=>ll)

GeoKit::GeoLoc also has lat and lng methods, so it will work too. By the way, GeoLoc is what gets returned when you make a geocoding call:

loc = GeoKit::Geocoders.GoogleGeocoder.geocode('94117')
Store.find(:all, :origin=>loc)

So far so good. What else has the appropriate methods? Well, anything that mixes in actsasmappable needs to have some sort of latitude/longitude methods. Say you already have an instance of Store -- you can pass the instance in as the origin:

starbucks=Store.find_by_name('Starbucks')
Store.find(:all, :origin=>starbucks)

In Summary . . .

There are three things you can pass into the :origin argument: 1) a two-element array containing latitude and longitude; 2) a geocodable string; 3) an object which will respond to lat/lng or latitude/longitude calls. Typically, you'll find yourself passing in LatLng or GeoLoc objects, or possibly other instances of whatever model you've mixed ActsAsMappable into.

Continue reading "GeoKit: all about the :origin argument

In-memory and in-database distance calculations

Feb 11 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.

Continue reading "In-memory and in-database distance calculations

GeoKit: a plugin for location-based Rails apps

Feb 09 by Andre

I am happy to release GeoKit over at RubyForge. GeoKit has been a collaboration between myself and Bill Eisenhauer

What is GeoKit?

Geokit is a Rails plugin for building location-based apps. It provides geocoding, location finders, and distance calculation in one cohesive package. If you have any tables with latitude/longitude oolumns in your database, or if you every wanted to easily query for "all the stores within a 50 mile radius," then GeoKit is for you.

What can GeoKit do for you?

  • Distance calculations between two points on the earth. Calculate the distance in miles or KM, with all the trigonometry abstracted away by GeoKit.
  • ActiveRecord distance-based finders. For example, you can find all the points in your database within a 50-mile radius.
  • Geocoding from multiple providers. It currently supports Google, Yahoo, Geocoder.us, and Geocoder.ca geocoders, and it provides a uniform response structure from all of them. It also provides a fail-over mechanism, in case your input fails to geocode in one service.
  • IP-based location lookup utilizing hostip.info. Provide an IP address, and get city name and latitude/longitude in return.
  • A before_filter helper to geocode the user's location based on IP address, and retain the location in a cookie.

Examples?

Find near latitude and longitude:

Store.find(:all, :origin =>  [37.792, -122.393] :conditions=>'distance<10')

Find near an address:

Store.find(:all, :origin=>'100 Spear st, San Francisco, CA', :conditions=>'distance<10')

Geocode an address:

res=GeoKit::Geocoders::GoogleGeocoder.geocode('100 Spear st, San Francisco, CA') 
puts res.lat 

Find distance:

distance=first_location.distance_from(second_location, :units=>:miles)

Where can you get it?

Head over to GeoKit at RubyForge for the plugin source and API docs.

Or, skip right to installing it as a plugin:

ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk

Continue reading "GeoKit: a plugin for location-based Rails apps

Rails, Geocoding, and Google Maps

Apr 18 by Andre
Update April '07: GeoKit, my Rails Geocoding plugin, abstracts away all the geocoding logic for multiple providers (Google, Yahoo Geocoder.us, Geocoder.ca -- including failover!), distance-based finders for ActiveRecord, and much more!
This is a simple example to demonstrate how to display a Google Map using Ruby on Rails, including utilizing a geocoding web service to translate addresses to geocodes.

Why the lookup web service? The Google Maps API doesn't just let you map an address. You need to supply the Maps API with a longitude and latitude to place a marker on a map (or even to center a map on a city). The geocoding web service translates a valid address into longitude and latitude; you can pass the result to the Maps API.

Continue reading "Rails, Geocoding, and Google Maps