Coder Social home page Coder Social logo

geokit's Introduction

## FEATURE SUMMARY

This plugin provides key functionality for location-oriented Rails applications:

- Distance calculations, for both flat and spherical environments. For example, 
  given the location of two points on the earth, you can calculate the miles/KM 
  between them.
- 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 geocoder the user's location based on IP address, 
  and retain the location in a cookie.
  
The goal of this plugin is to provide the common functionality for location-oriented 
applications (geocoding, location lookup, distance calculation) in an easy-to-use 
package.

## A NOTE ON TERMINOLOGY

Throughout the code and API of this, latitude and longitude are referred to as lat 
and lng.  We've found over the long term the abbreviation saves lots of typing time.

## DISTANCE CALCULATIONS AND QUERIES

If you want only distance calculation services, you need only mix in the Mappable 
module like so:

    class Location
    	include GeoKit::Mappable
    end

After doing so, you can do things like:

    Location.distance_between(from, to) 

with optional parameters :units and :formula.  Values for :units can be :miles or 
:kms with :miles as the default.  Values for :formula can be :sphere or :flat with
:sphere as the default.  :sphere gives you Haversine calculations, while :flat 
gives the Pythagoreum Theory.  These defaults persist through out the plug-in.

You can also do:

    location.distance_to(other)

The real power and utility of the plug-in is in its query support.  This is 
achieved through mixing into an ActiveRecord model object:

    class Location < ActiveRecord::Base
    	acts_as_mappable
    end

The plug-in uses the above-mentioned defaults, but can be modified to use 
different units and a different formulae.  This is done through the :default_units
and :default_formula keys which accept the same values as mentioned above.

The plug-in creates a calculated column and potentially a calculated condition.  
By default, these are known as "distance" but this can be changed through the
:distance_field_name key.  

So, an alternative invocation would look as below:

    class Location < ActiveRecord::Base
       acts_as_mappable :default_units => :kms, 
                        :default_formula => :flat, 
                        :distance_field_name => :distance
    end

You can also define alternative column names for latitude and longitude using
the :lat_column_name and :lng_column_name keys.  The defaults are 'lat' and
'lng' respectively.

Thereafter, a set of finder methods are made available.  Below are the 
different combinations:

Origin as a two-element array of latititude/longitude:

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

Origin as a geocodeable string:

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

Origin as an object which responds to lat and lng methods, 
or latitude and longitude methods, or whatever methods you have 
specified for lng_column_name and lat_column_name:

		find(:all, :origin=>my_store) # my_store.lat and my_store.lng methods exist

Often you will need to find within a certain distance. The prefered syntax is:

    find(:all, :origin => @somewhere, :within => 5)
    
. . . however these syntaxes will also work:

    find_within(5, :origin => @somewhere)
    find(:all, :origin => @somewhere, :conditions => "distance < 5")
    
Note however that the third form should be avoided. With either of the first two,
GeoKit automatically adds a bounding box to speed up the radial query in the database.
With the third form, it does not.

If you need to combine distance conditions with other conditions, you should do
so like this:

    find(:all, :origin => @somewhere, :within => 5, :conditions=>['state=?',state])

If :origin is not provided in the finder call, the find method 
works as normal.  Further, the key is removed
from the :options hash prior to invoking the superclass behavior.

Other convenience methods work intuitively and are as follows:

    find_within(distance, :origin => @somewhere)
    find_beyond(distance, :origin => @somewhere)
    find_closest(:origin => @somewhere)
    find_farthest(:origin => @somewhere)

where the options respect the defaults, but can be overridden if 
desired.  

Lastly, if all that is desired is the raw SQL for distance 
calculations, you can use the following:

    distance_sql(origin, units=default_units, formula=default_formula)

Thereafter, you are free to use it in find_by_sql as you wish.

There are methods available to enable you to get the count based upon
the find condition that you have provided.  These all work similarly to
the finders.  So for instance:

    count(:origin, :conditions => "distance < 5")
    count_within(distance, :origin => @somewhere)
    count_beyond(distance, :origin => @somewhere)

## FINDING WITHIN A BOUNDING BOX
 
If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map:

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

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 sw_point and ne_point from a map that is displayed on a web page.

If you need to calculate the bounding box from a point and radius, you can do that:

    bounds=Bounds.from_point_and_radius(home,5)
    Store.find :all, :bounds=>bounds

## USING INCLUDES

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)

In this case, you may want to just use the bounding box
condition alone in your SQL (there's no use calculating the distance twice):

    bounds=Bounds.from_point_and_radius(home,5)
    stores=Store.find :all, :include=>[:reviews,:cities] :bounds=>bounds
    stores.sort_by_distance_from(home)

## IP GEOCODING

You can obtain the location for an IP at any time using the geocoder
as in the following example:

    location = IpGeocoder.geocode('12.215.42.19')

where Location is a GeoLoc instance containing the latitude, 
longitude, city, state, and country code.  Also, the success 
value is true.

If the IP cannot be geocoded, a GeoLoc instance is returned with a
success value of false.  

It should be noted that the IP address needs to be visible to the
Rails application.  In other words, you need to ensure that the 
requesting IP address is forwarded by any front-end servers that
are out in front of the Rails app.  Otherwise, the IP will always
be that of the front-end server.

## IP GEOCODING HELPER

A class method called geocode_ip_address has been mixed into the 
ActionController::Base.  This enables before_filter style lookup of
the IP address.  Since it is a filter, it can accept any of the 
available filter options.

Usage is as below:

    class LocationAwareController < ActionController::Base
      geocode_ip_address
    end

A first-time lookup will result in the GeoLoc class being stored
in the session as :geo_location as well as in a cookie called
:geo_session.  Subsequent lookups will use the session value if it
exists or the cookie value if it doesn't exist.  The last resort is
to make a call to the web service.  Clients are free to manage the
cookie as they wish.

The intent of this feature is to be able to provide a good guess as
to a new visitor's location.

## INTEGRATED FIND AND GEOCODING

Geocoding has been integrated with the finders enabling you to pass
a physical address or an IP address.  This would look the following:

    Location.find_farthest(:origin => '217.15.10.9')
    Location.find_farthest(:origin => 'Irving, TX')

where the IP or physical address would be geocoded to a location and
then the resulting latitude and longitude coordinates would be used 
in the find.  This is not expected to be common usage, but it can be
done nevertheless.

## ADDRESS GEOCODING

GeoKit can geocode addresses using multiple geocodeing web services.
Currently, GeoKit supports Google, Yahoo, and Geocoder.us geocoding 
services. 

These geocoder services are made available through three classes: 
GoogleGeocoder, YahooGeocoder, and UsGeocoder.  Further, an additional
geocoder class called MultiGeocoder incorporates an ordered failover
sequence to increase the probability of successful geocoding.

All classes are called using the following signature:

    include GeoKit::Geocoders
    location = XxxGeocoder.geocode(address)

where you replace Xxx Geocoder with the appropriate class.  A GeoLoc
instance is the result of the call.  This class has a "success"
attribute which will be true if a successful geocoding occurred.  
If successful, the lat and lng properties will be populated.

Geocoders are named with the naming convention NameGeocoder.  This
naming convention enables Geocoder to auto-detect its sub-classes
in order to create methods called name_geocoder(address) so that
all geocoders are called through the base class.  This is done 
purely for convenience; the individual geocoder classes are expected
to be used independently.

The MultiGeocoder class requires the configuration of a provider
order which dictates what order to use the various geocoders.  Ordering
is done through the PROVIDER_ORDER constant found in environment.rb.

On installation, this plugin appends a template for your API keys to 
your environment.rb. 

Make sure your failover configuration matches the usage characteristics 
of your application -- for example, if you routinely get bogus input to 
geocode, your code will be much slower if you have to failover among 
multiple geocoders before determining that the input was in fact bogus. 

The Geocoder.geocode method returns a GeoLoc object. Basic usage:

    loc=Geocoder.geocode('100 Spear St, San Francisco, CA')
    if loc.success
      puts loc.lat
      puts loc.lng
      puts loc.full_address
    end

## INTEGRATED FIND WITH ADDRESS GEOCODING

Just has you can pass an IP address directly into an ActiveRecord finder
as the origin, you can also pass a physical address as the origin:

    Location.find_closest(:origin => '100 Spear st, San Francisco, CA')

where the physical address would be geocoded to a location and then the 
resulting latitude and longitude coordinates would be used in the 
find. 

Note that if the address fails to geocode, the find method will raise an 
ActiveRecord::GeocodeError you must be prepared to catch. Alternatively,
You can geocoder the address beforehand, and pass the resulting lat/lng
into the finder if successful.

## 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 equivilent 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.


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

## Cool stuff you can do with bounds
    
    bounds=Bounds.new(sw_point,ne_point)
    bounds.contains?(home)
    puts bounds.center
    

HOW TO . . .
=================================================================================

## How to install the GeoKit plugin 
    cd [APP_ROOT]
    ruby script/plugin install svn://rubyforge.org/var/svn/geokit/trunk
      or, to install as an external (your project must be version controlled):
    ruby script/plugin install -x svn://rubyforge.org/var/svn/geokit/trunk

## How to find all stores within a 10-mile radius of a given lat/lng
1. ensure your stores table has lat and lng columns with numeric or float 
   datatypes to store your latitude/longitude

3. use acts_as_mappable on your store model:
    class Store < ActiveRecord::Base
       acts_as_mappable
       ...
    end
3. finders now have extra capabilities:
    Store.find(:all, :origin =>[32.951613,-96.958444], :within=>10)

## How to geocode an address

1. configure your geocoder key(s) in environment.rb

2. also in environment.rb, make sure that PROVIDER_ORDER reflects the 
   geocoder(s). If you only want to use one geocoder, there should
   be only one symbol in the array. For example:
    PROVIDER_ORDER=[:google]
   
3. Test it out in script/console
    include GeoKit::Geocoders
    res = MultiGeocoder.geocode('100 Spear St, San Francisco, CA')
    puts res.lat
    puts res.lng
    puts res.full_address 
    ... etc. The return type is GeoLoc, see the API for 
    all the methods you can call on it.

## How to find all stores within 10 miles of a given address

1. as above, ensure your table has the lat/lng columns, and you've
   applied acts_as_mappable to the Store model.

2. configure and test out your geocoder, as above

3. pass the address in under the :origin key
		Store.find(:all, :origin=>'100 Spear st, San Francisco, CA', 
		           :within=>10)

4. you can also use a zipcode, or anything else that's geocodable:
		Store.find(:all, :origin=>'94117', 
		           :conditions=>'distance<10')

## How to sort a query by distance from an origin

You now have access to a 'distance' column, and you can use it
as you would any other column. For example:
		Store.find(:all, :origin=>'94117', :order=>'distance')

## How to elements of an array according to distance from a common point

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.

Database Compatability
=================================================================================
GeoKit does *not* work with SQLite, as it lacks the necessary geometry functions. 
GeoKit works with MySQL (tested with version 5.0.41) or PostgreSQL (tested with version 8.2.6)
GeoKit is known to *not* work with Postgres <8.1 -- it uses the least() funciton.


HIGH-LEVEL NOTES ON WHAT'S WHERE
=================================================================================

acts_as_mappable.rb, as you'd expect, contains the ActsAsMappable
module which gets mixed into your models to provide the 
location-based finder goodness.

mappable.rb contains the Mappable module, which provides basic
distance calculation methods, i.e., calculating the distance
between two points. 

mappable.rb also contains LatLng, GeoLoc, and Bounds.
LatLng is a simple container for latitude and longitude, but 
it's made more powerful by mixing in the above-mentioned Mappable
module -- therefore, you can calculate easily the distance between two
LatLng ojbects with distance = first.distance_to(other)

GeoLoc (also in mappable.rb) represents an address or location which
has been geocoded. You can get the city, zipcode, street address, etc.
from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng
AND the Mappable modeule goodness for free.

geocoders.rb contains the geocoder classes.

ip_geocode_lookup.rb contains the before_filter helper method which
enables auto lookup of the requesting IP address.

## IMPORTANT NOTE: We have appended to your environment.rb file

Installation of this plugin has appended an API key template 
to your environment.rb file. You *must* add your own keys for the various
geocoding services if you want to use geocoding. If you need to refer to the original
template again, see the api_keys_template file in the root of the plugin.

geokit's People

Stargazers

Pritesh avatar

Watchers

Pritesh avatar James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.