New GeoKit Release
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.