Daniel Azuma

Geo-Rails Part 2: Setting Up a Geospatial Rails App

georails

Before going in depth into any particular topic, I thought it would be useful to write a getting-started tutorial, walking through setting up and working with a simple example Rails app using RGeo. In this tutorial, we will:

  • Install the main software components we need for a geospatial application, including a spatial database and the needed Ruby libraries.
  • Set up a new Rails application configured to use the spatial database.
  • Create an ActiveRecord model with a spatial attribute.
  • Experiment with location data in the model.
  • Perform simple spatial queries.

This should help you get started writing basic location features in Rails, giving you a feel for what the tools are and how they fit together.

This is part 2 of my series of articles on geospatial programming in Ruby and Rails. For a list of the other installments, please visit http://daniel-azuma.com/articles/georails.

Installing the software

First, you'll need a spatial database. Recent versions of MySQL have basic spatial features built in, but I generally recommend using PostgreSQL and installing the PostGIS add-on, because PostGIS is a much more feature complete and better supported product. For the rest of this example, I'll assume you're using PostGIS.

If you have a package manager such as apt-get, yum, MacPorts, or Homebrew, that's usually the easiest way to install PostGIS. Your package manager should also install PostGIS's dependencies, including PostgreSQL itself and two important libraries: GEOS and Proj.

If you need to build and install from source, you can download PostGIS from http://www.postgis.org/ and review the instructions in the documentation section there. You will first need to build and install PostgreSQL (http://www.postgresql.org/), GEOS (http://geos.osgeo.org/) and Proj (http://proj.osgeo.org/). The build configuration for PostGIS requires that you provide the paths to your installations of those three dependencies.

We'll use the latest Rails 3.1 release for this example. Most of the gems you require will come with a simple gem install rails. However, to hook up with PostGIS, you'll need a few more gems. Installing these gems will require you to know where the database software was installed. Specifically, you need the --prefix for the GEOS and Proj libraries, and you need the path to the pg_config binary in your PostgreSQL installation.

Obviously, these locations will depend on how you installed the software. If you used MacPorts on Mac OS X, things get installed in /opt/local. So here are the gem install commands for that case. If you used a different system, you'll need to modify the paths accordingly.

% gem install pg -- --with-pg-config=/opt/local/bin/pg_config
% gem install rgeo -- --with-geos-dir=/opt/local \
    --with-proj-dir=/opt/local
% gem install activerecord-postgis-adapter

The main gem you just installed is activerecord-postgis-adapter, which provides a special ActiveRecord adapter for talking to PostGIS and handling geospatial data.

Setting up the Rails app

Now, we'll build our Rails app. I'll assume you're using the latest Rails 3.1.

% rails new geo_rails_test -d postgresql

Rails out of the box doesn't know about PostGIS, but if we set the database initially to PostgreSQL, Rails will be able to set up most of the configuration for you. Now we just need to do some tweaking.

Add the PostGIS ActiveRecord adapter to your Gemfile, by inserting this line:

gem 'activerecord-postgis-adapter'

Install the adapter into your Rails app by inserting this line into config/application.rb, just after the require 'rails/all' line:

require 'active_record/connection_adapters/postgis_adapter/railtie'

Now we need to modify config/database.yml to use the PostGIS adapter. There are several different strategies for handling the database, but I'll choose a simple one to get you started. For each of the environments in your database.yml, do the following:

First, change the "adapter" from "postgresql" to "postgis".

Next, add the following line to each environment:

schema_search_path: "public,postgis"

Next, add the following line to your development and test environments. The path should match the scripts directory installed by PostGIS. Assuming you used MacPorts and installed PostGIS 1.5.x, the path will probably be as follows, but you should modify it depending on where PostGIS was installed:

script_dir: /opt/local/share/contrib/postgis-1.5

You'll set up two users in the database. One is the normal database user that your Rails app will connect as. The other needs to be a superuser, and is used to create the databases initially (i.e. using rake db:create). Creating a PostGIS database requires superuser privileges unless you set up a special template for it, and for simplicity, we won't do that right now.

Your normal user should be specified by the "username" and "password" properties:

username: geo_rails_test
password: <your-password>

The superuser for creating the database should be specified by the "su_username" and "su_password" properties:

su_username: geo_rails_test_creator
su_password: <your-password>

You will, of course, also need to create these users in the database. You can do so by running the PostgreSQL "createuser" command:

% createuser --pwprompt --superuser geo_rails_test_creator
% createuser --pwprompt geo_rails_test

Now, you should be able to create the database. A PostGIS database has several internal tables and a bunch of functions and other objects built in. If the configuration above has been set up correctly, the activerecord-postgis-adapter should be able to create all of this internal structure for you automatically.

% rake db:create

The hard part is over! Now we can start building models with location features.

A model with location

Most databases will support the familiar data types such as strings, integers, floating-point values, booleans, and the like. Spatial databases are simply normal databases that include support for several additional data types commonly used to represent location. These data types can include points on the surface of the earth (latitude and longitude) as well as lines, polygons, and other shapes. RGeo, together with the activerecord-postgis-adapter, lets you interact with these data types just like you would any other attribute on an ActiveRecord object.

For this tutorial, we will create a simple model that includes a location. If you have represented location in an database before, you may have simply added two columns to your database, one for latitude and one for longitude. In a spatial database, you represent a location with a single column whose type is "point". A point type is simply an ordered pair of latitude and longitude.

% rails generate model Location name:string latlon:point

Before we migrate the database, let's look at the migration that was generated. It should look something like this:

class CreateLocations < ActiveRecord::Migration
  def change
    create_table :locations do |t|
      t.string :name
      t.point :latlon
      t.timestamps
    end
  end
end

Notice that the "latlon" column is of type "point". Spatial data types such as "point" often have further configurations that you may need to apply-- similar to how, for example, string columns can have length limits, or other types can have various constraints. In our case we want the latlon column to contain not just any two-dimensional coordinate, but specifically a latitude and longitude. To specify this, we will add the "geographic" constraint to it. Change the "latlon" column in the migration and add ":geographic => true". Now your migration should look like:

class CreateLocations < ActiveRecord::Migration
  def change
    create_table :locations do |t|
      t.string :name
      t.point :latlon, :geographic => true
      t.timestamps
    end
  end
end

One last thing we should do is configure the model class so it knows what kind of geospatial data you are storing. For the most part, the activerecord-postgis-adapter is able to infer most of this information from the database. However, this inference is not perfect, and anyway it is generally good practice to set this explicitly, for documentation sake if for nothing else.

Open your app/models/location.rb and add a line to the Location class:

class Location < ActiveRecord::Base
  set_rgeo_factory_for_column(:latlon,
    RGeo::Geographic.spherical_factory(:srid => 4326))
end

That line says, for the "latlon" field, use a spherical geographic coordinate system with spatial reference ID 4326. This means, computations done in Ruby will assume a spherical earth, and the spatial reference ID should be set to 4326 to match what PostGIS expects for a "geographic" column. In many cases, you can configure each geographic column in this same way. For now, don't worry too much about the details. We'll cover coordinate systems and spatial references in a later article.

Now run the migration to get this table into your database.

% rake db:migrate

Now that we've got the model set up and the database migrated, let's take a look into the actual location data in the model.

Working with location data

For simplicity, let's dive into the rails console to start playing with our new model.

% rails console
Loading development environment (Rails 3.1.3)
ruby-1.9.3-p0 :001 >

An ActiveRecord model with spatial data is just the same as any other ActiveRecord model. We can create and start working with it directly in the console:

ruby-1.9.3-p0 :001 > loc = Location.create
 => #<Location id: 1, name: nil, latlon: nil, created_at: "2011-11-28 02:52:10",
     updated_at: "2011-11-28 02:52:10">

Our model has two attributes, the "name" string and the "latlon" point. They started off as nil, but we can set them.

ruby-1.9.3-p0 :002 > loc.name = "Pirq Headquarters"
ruby-1.9.3-p0 :003 > loc.latlon = "POINT(-122.193963 47.675086)"

Note the string that we used to set the latlon field. This is a standard syntax called "WKT" (Well-Known Text), which is commonly used in spatial applications. In the WKT representation of a location, notice that the longitude comes first, and there is no comma between longitude and latitude. The model understands WKT syntax when you set data, but internally converts it to a "point" object.

ruby-1.9.3-p0 :004 > loc.latlon
 => #<RGeo::Geographic::SphericalPointImpl:0x817d61f4
     "POINT (-122.193963 47.675086)">

A point object is one of the spatial Ruby classes provided by RGeo. Using these spatial classes, you can perform powerful geometric and geographic computations and analyses, or you can simply use them to pass data around. We will cover some of their capabilities in later entries in this blog series. For now, here's a quick example, measuring the distance between two Location objects:

ruby-1.9.3-p0 :005 > loc2 = Location.create(:name => 'Space Needle',
                        :latlon => 'POINT(-122.349341 47.620471)')
ruby-1.9.3-p0 :006 > puts "Distance is %.02f meters" %
                      loc.latlon.distance(loc2.latlon)
Distance is 13143.18 meters

You do not have to set a spatial field using WKT; you can also set it directly using a spatial object such as a point object. For example, you can set the Pirq Headquarters location to be the same as the Space Needle location:

ruby-1.9.3-p0 :007 > loc.latlon = loc2.latlon
 => #<RGeo::Geographic::SphericalPointImpl:0x8175f234
     "POINT (-122.349341 47.620471)">

Spatial attributes are loaded and saved in the same way as any other attribute on your model. So until you save the "loc" object, the latlon value in the database remains unchanged.

Querying by location

The real power of a spatial database such as PostGIS comes from its query capabilities. Spatial databases typically provide a rich set of SQL functions that you can use to build a wide variety of location-based queries.

Let's go through a couple of simple examples, querying against the two locations we just created. (We'll assume you didn't save loc in the previous example, so the two model objects still have their different latlon values.)

These first two queries find the objects, respectively less than and greater than 10 kilometers from a particular point (the location of the Columbia Tower in downtown Seattle).

ruby-1.9.3-p0 :008 > Location.where("ST_Distance(latlon, "+
                   "'POINT(-122.330779 47.604828)') < 10000").
                   map{ |ar| ar.name }
 => ["Space Needle"]
ruby-1.9.3-p0 :009 > Location.where("ST_Distance(latlon, "+
                   "'POINT(-122.330779 47.604828)') > 10000").
                   map{ |ar| ar.name }
 => ["Pirq Headquarters"]

The following query draws a triangle and finds the objects within the triangle.

ruby-1.9.3-p0 :010 > Location.where("ST_Intersects(latlon, "+
                   "'POLYGON((-122.19 47.68, -122.2 47.675, "+
                   "-122.19 47.67, -122.19 47.68))')").
                   map{ |ar| ar.name }
 => ["Pirq Headquarters"]

See the PostGIS documentation for a full set of the various spatial SQL functions you can use in your queries.

Once you have a lot of data in your database, you'll need to add a spatial index to speed up location queries, similar to how you index any other column. However, there are some nuances in how you construct a spatial index, and especially how you should write queries to take advantage of a spatial index. This involves a separate discussion that I'll cover in a later article.

Where to go from here

This has been a bit of a long entry, but an important first step towards working with spatial data in Rails. We've gone through setting up a Rails application with spatial capabilities, and investigated a few of the basic ways in which we can store, manipulate, and query spatial data.

You may find it useful to start looking through the documentation for PostGIS. This will give more detailed information about the how the database handles and queries spatial data. The readme for the activerecord-postgis-adapter gem provides more information about configuring the database connection from Rails.

In upcoming articles, we'll start looking a little more deeply at the various topics we've touched on here, including working with RGeo's spatial Ruby classes, working with coordinate systems, and setting up spatial indexes. Stay tuned, and let's bring Rails down to earth!

This is part 2 of my series of articles on geospatial programming in Ruby and Rails. For a list of the other installments, please visit http://daniel-azuma.com/articles/georails.

Dialogue & Discussion