« June 2007 | Main | September 2007 »

New GeoKit Release

Jul 22 by Andre in GeoKit »

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.