Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / PostgreSQL

Creating a Location Aware Website using Ruby on Rails and PostGIS

0.00/5 (No votes)
9 Jan 2012CPOL4 min read 26.7K  
How to create a location aware website using Ruby on Rails, PostgreSQL, and PostGIS

PostGIS is a geospatial extension to PostgreSQL which gives a bunch of functions to handle geospatial data and queries, e.g., to find points of interest near a certain location, or storing a navigational route in your database. You can find the PostGIS documentation here.

In this example, I’ll show how to create a location aware website using Ruby on Rails, PostgreSQL, and PostGIS. The application, when finished, will be able to store your current location – or a check-in – in the database, show all your check-ins on a map, and show check-ins nearby check-ins.

This app is written in Rails 3.1 but it could just as well be written in another version. As of writing, the current version of the spatial_adapter gem has an issue in Rails 3.1 but we will create a workaround for this until it gets fixed.

You can view the complete source code or see the final application in action.

Creating the PostGIS Enabled Database

We will first create our geospatially enabled database. First, check out my post on installing PostgreSQL and PostGIS on Mac OS X.

Create your database:

Bash
$ createdb -h localhost my_checkins_development

Install PostGIS in your database:

Bash
$ cd /opt/local/share/postgresql90/contrib/postgis-1.5/
$ psql -d my_checkins_development -f postgis.sql -h localhost
$ psql -d my_checkins_development -f spatial_ref_sys.sql -h localhost

Your database is now ready for geospatial queries.

Creating the Geospatially Enabled Rails App

Create your app:

Bash
$ rails new my_checkins

The spatial_adapter gem is a plug in that adds geospatial functionality to Rails when using a PostgreSQL and PostGIS. It uses GeoRuby for data types. Add this and the pg (Postgres) gem to your Gemfile:

Ruby
gem 'spatial_adapter'
gem 'pg'

Run bundle install:

Bash
$ bundle install

Setup your config/database.yml:

Ruby
development:
  adapter: postgresql
  database: my_checkins_development
  host: localhost

And your app is geospatially enabled. :)

Creating the Code to Handle Check-ins

Let’s create some scaffold code to handle our check-ins:

Bash
$ rails g scaffold checkin title:string location:point

Take notice of the point data type – that’s a geospatial type.
Before running your migrations, edit db/migrate/create_checkins.rb, replacing this:

Ruby
t.point :location

with this:

Ruby
t.point :location, :geographic => true

This tells your migration to add a geographic column that is set up to handle geographic coordinates, also known as latitudes and longitudes.

Run your migrations:

Bash
$ rake db:migrate

We are now ready to store our check-ins.

The Checkin model now contains a location field which is a data type of GeoRuby::SimpleFeatures::Point. This data type has properties of x and y. We will expose these as properties directly on the model. In app/models/checkin.rb:

Ruby
class Checkin < ActiveRecord::Base
  def latitude
    (self.location ||= Point.new).y
  end

  def latitude=(value)
    (self.location ||= Point.new).y = value
  end

  def longitude
    (self.location ||= Point.new).x
  end

  def longitude=(value)
    (self.location ||= Point.new).x = value
  end
end

Latitude and longitude are now exposed.

In app/views/checkins/_form.html.erb, replace this:

HTML
<div class="field">
  <%= f.label :location %><br />
  <%= f.text_field :location %>
</div>

With this:

Ruby
<div class="field">
  <%= f.label :latitude %><br />
  <%= f.text_field :latitude %>
</div>
<div class="field">
  <%= f.label :longitude %><br />
  <%= f.text_field :longitude %>
</div>

If it wasn’t for a little bug in spatial_adapter under Rails 3.1, we would now be able to save locations from our Rails app. However, what the bug does is that it cannot create records when the location field is set. It can update them so what we will do is to make sure it first creates the check-in with a location set to nil and then updates it with the correct location. Like this, in app/controllers/checkins_controller.rb in the create method, replace this:

Ruby
def create
  ...
  if @checkin.save
    ...

with this:

Ruby
def create
  ...
  if @checkin.valid?
    location = @checkin.location
    @checkin.location = nil
    @checkin.save!
    @checkin.location = location
    @checkin.save!
    ...

And it should work.
Try and fire up your server:

Bash
$ rails s

And go to http://localhost:3000/checkins/new in your browser.

Next, in app/views/checkins/show.html.erb, replace this:

HTML
<p>
  <b>Location:</b>
  <%= @checkin.location %>
</p>

with this:

HTML
<p>
  <b>Location:</b>
  <%= @checkin.latitude %>, <%= @checkin.longitude %>
</p>

And it will show the latitude and longitude you just entered.

Getting Our Current Location

We would like to be able to create check-ins from our current location. Modern browsers expose this functionality via a JavaScript API. Create app/assets/javascripts/checkins.js and add this:

JavaScript
function findMe() {
  if(navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(function(position) {
      document.getElementById('checkin_latitude').value = position.coords.latitude;
      document.getElementById('checkin_longitude').value = position.coords.longitude;
    }, function() {
      alert('We couldn\'t find your position.');
    });
  } else {
    alert('Your browser doesn\'t support geolocation.');
  }
}

And a button in the top of app/views/checkins/_form.html.erb:

Ruby
<input type="button" value="Find me!" onclick="findMe();" />

Try it in your browser. If it gives you a JavaScript error saying the findMe method isn’t defined, try restarting your server to get the new JavaScript loaded. You should now be able to get your current location by clicking the Find me! button.

Finding Nearby Check-ins

Let’s create a method for finding nearby check-ins. PostGIS has a function named ST_DWithin which returns true if two locations are within a certain distance of each other. In app/models/checkin.rb, add the following to the top of the class:

Ruby
class Checkin < ActiveRecord::Base
  scope :nearby_to,
    lambda { |location, max_distance|
      where("ST_DWithin(location, ?, ?) AND id != ?", checkin.location, max_distance, checkin.id)
    }
  ...

In app/controllers/checkins_controller.rb, add the following:

Ruby
def show
  @checkin = Checkin.find(params[:id])
  @nearby_checkins = Checkin.nearby_to(@checkin, 1000)
  ...

In app/views/checkins/show.html.erb, add the following just before the links in the bottom:

Ruby
<h2>Nearby check-ins</h2>
<ul>
  <% @nearby_checkins.each do |checkin| %>
    <li><%= link_to checkin.title, checkin %></li>
  <% end %>
</ul>

It now shows all nearby checkins. Try adding a couple more based on your current location and see it in action.

Creating a Map With All Check-ins

Wouldn’t it be nice to show all our check-in on a map? We will do this using the Google Maps API.

In app/views/checkins/index.html.erb, clear out the table and list, and add the following:

Ruby
<script type="text/javascript" 
src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>

That loads the Google Maps JavaScript API functionality.

Create a div for the map:

Ruby
<div id="map" style="width: 600px; height: 500px;"></div>

And add the following script at the bottom:

JavaScript
<script type="text/javascript">
  // Create the map
  var map = new google.maps.Map(document.getElementById("map"), {
    mapTypeId: google.maps.MapTypeId.ROADMAP
  });

  // Initialize the bounds container
  var bounds = new google.maps.LatLngBounds();

  <% @checkins.each do |checkin| %>
    // Create the LatLng
    var latLng = new google.maps.LatLng(<%= checkin.latitude %>, <%= checkin.longitude %>);

    // Create the marker
    var marker = new google.maps.Marker({
        position: latLng,
        map: map,
        title: '<%= escape_javascript(checkin.title) %>'
    });

    // Add click event
    google.maps.event.addListener(marker, 'click', function() {
      document.location = '<%= checkin_path(checkin) %>';
    });

    // Extend the bounds
    bounds.extend(latLng);
  <% end %>

  // Fit to bounds
  map.fitBounds(bounds);
</script>

There’s our map.:) Check it out at http://localhost:3000/checkins. Try creating some check-ins around the world to see the map expand.

Conclusion

That’s a location aware app that stores check-ins based on our current location, shows nearby check-ins, and displays check-ins on a map.

View the complete source code or see the final application in action.

I'd love to hear your thoughts!

Write a comment, Follow me on Twitter, or Subscribe to my feed.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)