Kingpin

Kingpin is a Ruby gem that provides seamless integration of Pincaster into every ActiveRecord model in Rails 2 and 3.

Pincaster is a very fast memory driven but persisting NoSQL (yeah..) DB engine for fast geolocation operations. Kingpin directly integrates Pincaster into every ActiveRecord model that has at least real or virtual attributes for latitude and longitude. After indexing all of your objects you can easily request objects around your query object. Kingpin therefore provides several methods as well as named scopes and perfectly maps all operations that usually had to be taken manually.

Pincaster supports different layers that can be used to distinguish the kind of objects to be stored. Kingpin automatically creates a layer per ActiveRecord model and uses the class name as layer name. Every indexed AR object then is provided with a related Pincaster object - in this scope called a 'pin'. These pins can be used for fast nearby retrieval as well as distance calculation.

Prerequisites

  • Install Frank Denis's Pincaster
  • Configure Pincaster according to your needs
  • Choose a proper 'Accuracy' method in pincaster.conf / I recommend: 'rhomboid'
  • Startup Pincaster

Installation

Kingpin is a gem available via Rubygems.org. To install it, simply put it into your Rails3 app's Gemfile:

gem 'kingpin', '0.x.0'

and afterwards run bundler:

bundle install

In case you are on Rails 2 without bundler support add Kingpin to your environment.rb as you are used to with other gems and install it via:

rake gems:install

or:

gem install kingpin

Congrats, you're done!

Configuration

Kingpin comes with it's own default configuration file that is loaded in case none is provided by the user. Create your own one at:

config/kingpin.yml

with YAML formatted content like that:

---
protocol: http
host: localhost
port: 4269
namespace: '/api/1.0'

Do not forget the '- - -' and don't mess with spaces or tabs, it's YAML. Adjust the values to your needs. Most recent versions of Pincaster support http only afaik, so you should not change it. Same with the namespace, changing it results in 404 responses for your requests.

Integration

Integration into you models is straight forward. Assuming that your model has an attribute_reader or accessor_method like one of the following: [:latitude, :lati, :ltt, :ltd, :lat, :lttd]

as well as one of the following for longitude:

[:longitude, :long, :lng, :lgt, :lgtd, :lngtd]

and the values are in DEG not RAD, your way of integration looks like that:

class FooWithLocation
  pinnable
  ...
end

Either way, Kingpin tries to minimize your pain, so some configuration options are provided as well:

  • :methods => {:lat => :your_latitude, :lng => :your_longitude} in case your location accessors have different names
  • :rad => true defaults to false if not mentioned, and assumes your lat and lng in RAD instead of DEG
  • :autopin => true automatically generates a Pincaster record every time an instance of an enabled model is saved
  • :include => :all | {:only => [:attr_1, :attr_2, ...]} | {:except => [:attr_1, :attr_2, ...]}

Example: In case your longitude resides at :cool_longitude, your latitude at :cool_latitude, the values are stored in RAD and you would like to automatically create Pincaster pins, your classes head should look like this:

class FooWithLocation
  pinnable :methods => { :lat => :cool_latitude, :lng => :cool_longitude }, :rad => true, :autopin => true
  ...
end

In case you would like to integrate the instances values for :name and :title only into the pin, your class definition looked like that:

class FooWithLocation
  pinnable :include => {:only => [:name, :title]}
  ...
end

Vice versa :include => {:except => [:name, :title]} would integrate all attributes but :name and :title, :include => :all integrated all available attributes of the instance.

Indexing

Next you should create some pins. The most recent version of Kingpin does not provide a Raketask for that automatically, but it will in the next release. You could create one on your own and make it look like the following:

namespace :kingpin do
  desc "Rebuild Pincaster index for FooWithLocations"
  task :reindex_foo_with_locations => :environment do
    if Pincaster.is_alive?
      1.upto(FooWithLocation.find(:last).id) do |i|
        begin
          STDOUT.print "indexed: " + i.to_s + "\r"
          FooWithLocation.find(i).add_pin
          STDOUT.flush
        rescue
        end
      end
    else
      puts "Pincaster seems to be down!\n"
    end
    puts "Finished reindexing FooWithLocations.\n"
  end
end

Invocation

Kingpin comes with some high- and some low-level operations you can use.

Highlevel operations

Instance methods

  • nearby_ids(n,limit) - returns the id's of those foo's which are in range of n meters
  • nearby(n,limit) - returns an array of fully fledged AR records within a range of n meters

Please have a look at the Pincaster doc's to fully understand how limit works. It's not supposed to work like the typical SQL like limit. If ommitted it defaults to 10000.

Example:

foo = FooWithLocation.find(4711)
foo.nearby_ids(500)

Example:

foo = FooWithLocation.find(42)
foo.nearby(500)

Class methods - speak (named) scope

  • nearby(location, n)

nearby returns a named scope (sorry, no Rails3 scope available yet) that can be chained as you are used to.

Example:

foo = FooWithLocation.find(23)
FooWithLocation.nearby(foo, 500)

Low level operations

You would like to gain control over pins to be created? No problem. Kingpin provides the following instance methods for every enabled ActiveRecord model:

foo = FooWithLocation.find(23)
  • foo.add_pin - adds a pin for self
  • foo.pin - returns an existing pin for self or nil
  • foo.delete_pin! - deletes an existing pin
  • foo.nearby_ids(radius, limit) - returns an array of AR instance id's in given radius of self
  • foo.nearby(radius, limit) - returns an array of AR objects in given radius of self
  • foo.pin_lat - returns self's normalized latitude (in DEG)
  • foo.pin_lng - returns self's normalized longitude (in DEG)

There are also some Pincaster related methods that are bound to a publicly available Pincaster class. These read as follows:

  • Pincaster.is_alive? - returns true or false, depending on Pincaster server's state
  • Pincaster.shutdown! - cleanly shut's down Pincaster, so every in-memory-only-data can be persited before exit
  • Pincaster.layers - returns an array of strings representing the available layers (AR class names of indexed models)
  • Pincaster.has_layer(string) - returns true or false depending of existance of the given layer
  • Pincaster.add_layer(string) - adds a layer with given string as name
  • Pincaster.delete_layer!(string) - deletes the layer with the given name
  • Pincaster.layer(string) - return a Kingpin layer object that can be used to retrieve some more information about a layer
  • Pincaster.config - returns a Kingpin config objects that hold's Pincasters config

Interesting layer related methods are:

layer = Pincaster.layer("FooWithLocation")
  • layer.name - returns layers name
  • layer.records - returns the number of records (pin's) in that very layer
  • layer.geo_records - returns the number of records in that layer having geo data included
  • layer.distance_accuracy - returns the distance accuracy level
  • layer.type - returns the layers type (please have a look at the Pincaster doc's here)
  • layer.bounds - returns the layers bounds

Coming soon

Kingpin is missing some functionality yet, but this will arrive soon:

  • support for rectangle search
  • distance integration into every returned AR record at retrieval time
  • automated Raketask for index creation
  • tests

Authors

Kingpin was written by:

Jan Roesner mailto: [email protected]

for use at Friendticker

Thanks

  • Frank Denis for his support
  • Anke for moral support
  • the Friendticker staff

Contributing to Kingpin

  • Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  • Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
  • Send me a pull request. Bonus points for topic branches.

Copyright (c) 2011 Jan Roesner. See LICENSE.txt for further details.