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:
$ createdb -h localhost my_checkins_development
Install PostGIS in your database:
$ 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:
$ 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:
gem 'spatial_adapter'
gem 'pg'
Run bundle install
:
$ bundle install
Setup your config/database.yml:
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:
$ 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:
t.point :location
with this:
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:
$ 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:
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:
<div class="field">
<%= f.label :location %><br />
<%= f.text_field :location %>
</div>
With this:
<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:
def create
...
if @checkin.save
...
with this:
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:
$ rails s
And go to http://localhost:3000/checkins/new in your browser.
Next, in app/views/checkins/show.html.erb, replace this:
<p>
<b>Location:</b>
<%= @checkin.location %>
</p>
with this:
<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:
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:
<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:
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:
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:
<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:
<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:
<div id="map" style="width: 600px; height: 500px;"></div>
And add the following script
at the bottom:
<script type="text/javascript">
var map = new google.maps.Map(document.getElementById("map"), {
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var bounds = new google.maps.LatLngBounds();
<% @checkins.each do |checkin| %>
var latLng = new google.maps.LatLng(<%= checkin.latitude %>, <%= checkin.longitude %>);
var marker = new google.maps.Marker({
position: latLng,
map: map,
title: '<%= escape_javascript(checkin.title) %>'
});
google.maps.event.addListener(marker, 'click', function() {
document.location = '<%= checkin_path(checkin) %>';
});
bounds.extend(latLng);
<% end %>
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.