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 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 plugin 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 exposes 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">
// 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.
Related posts
Tags: Geospatial, PostGIS, PostgreSQL, Ruby on Rails
You may want to checkout https://github.com/dazuma/rgeo ;)
Its not my first time to visit this web page, i am browsing this web page dailly and obtain pleasant information from here everyday.
I was really looking forward to see this application in action, but it seems it has an issue right now, because it’s down. Could you please fix it? other than that, great work. a fine tutorial, sir.
@Jose: Would like to get it up and running again but can’t at the moments as I’m having a problem with PostGIS. Sorry :/
Ok, no problem, I’m almost done with your tutorial, really awesome,
Except I’m having a little issue, you may be able to help me with:
When I try to save a new Checkin:
An exception is being raised:
TypeError in CheckinsController#create
can’t convert String into Float
Parameters:
{“utf8″=>”✓”,
“authenticity_token”=>”DEEcWF/90AlyCG5ORBuZp3zu2AqXPjE8llMDAWsxeHo=”,
“checkin”=>{“title”=>”Home”,
“latitude”=>”-0.1774961″,
“longitude”=>”-78.4742372″},
“commit”=>”Save”}
Is there something I’m missing here? because I have followed (copied and pasted your code from github) strictly.
Have a nice day!
@Jose: Glad you like it :)
Could you tell me the line number and what’s in that line?
Thanks,
/Lasse
Sure,
This would be more detailed I think:
TypeError in CheckinsController#create
can’t convert String into Float
app/controllers/checkins_controller.rb:52:in `block in create’
app/controllers/checkins_controller.rb:46:in `create’
Parameters:
{“utf8″=>”✓”,
“authenticity_token”=>”DEEcWF/90AlyCG5ORBuZp3zu2AqXPjE8llMDAWsxeHo=”,
“checkin”=>{“title”=>”Home”,
“latitude”=>”-0.1774961″,
“longitude”=>”-78.4742372″},
“commit”=>”Save”}
Here’s the controller’s action:
#def create is line 43
def create
@checkin = Checkin.new(params[:checkin])
respond_to do |format|
if @checkin.valid?
location = @checkin.location
@checkin.location = nil
@checkin.save!
@checkin.location = location
@checkin.save!
format.html { redirect_to @checkin, :notice => ‘Checkin was successfully created.’ }
format.json { render :json => @checkin, :status => :created, :location => @checkin }
else
format.html { render action: “new” }
format.json { render json: @checkin.errors, status: :unprocessable_entity }
end
end
end
Let me know if you need more code, although… like I said, It’s identical to yours as seen on GitHub(Except I use HAML)
Thank you very much!
Just a guess, but in app/models/checkin.rb, try replacing the two ” = value” with ” = value.to_f”
Yes Sir !
That definitely worked! thanks, I’m sorry If this is such a n00b issue, I’m totally new to PostGis.
Anyways, I wanted to point out something else,
In the index view, we can see the “Listing checkins” right….
for each Checkin we have a title and a Location. I don’t know if what I’m seeing is correct, The row for my Home checkin looks like this:
Home # Show Edit Destroy
Where Location is (obviously) this GeoRuby::Simple::Features::Point object STRING.
is it supposed to render Location in this way in the view? (I’m guesing that the answer is no), Is there a way to fix this as well? (or perhaps I did something wrong :S)
Thank very much! You Rock!
Just in case,
The marker is showing up in the map correctly.
Which view is that? :)
Hmmm, the important part didn’t show back two comments ago, I sent you an email, I think that’s better,
Thanks a bunch Sir!
Thanks! Just looked at it – you can just replace < %= xx.location %> with < %= xx.location.y %>, < %= xx.location.x %> – then it would work :)
/Lasse
Great! That worked very well, Thank you for this lesson, There’s no better tutorial on PostGis and Google maps for rails anywhere else! Congrats!
One final thing, I just can’t help asking. In the coming days, I’ll probably need a hosting service to deploy an application like this one, Which one do you recommend? I believe Engine Yard allows to have an Instance of PostGis, but it’s not free. There are many possibilities out there, but since it’s quite clear you’re an expert on the topic, Please advice me on where to host an App like this, that uses PostGres and PostGis.
Thank you very very much once more !
Sure :) What I would do is find a hosting provider that allows you to create a VPS – Virtual Private Server. That’s a very cheap solution (10-20$/month) if you know how – or want to try – to build a Linux server. I have tried RackSpace which works really great if you’re near USA: http://www.rackspace.com/cloud/cloud_hosting_products/servers/
Would this work for you?
Oh yes !
That’s it. it’s very similar to Linode, right? I agree, a VPS it’s the way to go. I needed your opinion, because for this case it seems full control it’s the way it’s the only way.
Anyways, I’ll let you go for now, You’ve have been really awesome. Can’t thank you enough. Good luck in all your endeavours, and hope to read from you soon!
Excellent work, keep it up!
Sincerely,
Jose.
Yeah I think it’s practically the same :) I used to have a Slicehost (now bought by RackSpace) account but then decided to try running it from home instead. But then I tried RackSpace later on at it worked just fine :)
Please do send me a link when you’ve got something running :)
/Lasse
Sure. no problem. I’ll provide you the link (eventually) :P
It’s the least I could do !
Nice to meet you.
Jose.
Sounds good :)
Nice to meet you too :)
/Lasse
Simply want to say your article is as astonishing. The clarity on your submit is just great and i could suppose you’re knowledgeable in this subject. Well with your permission allow me to seize your RSS feed to keep updated with coming near near post. Thanks one million and please continue the gratifying work.
Hi there, great Article, but I run into a Problem, I wasn´t able to solve:
With Ruby 1.9.3 and Rails 3.2.2, i get the Error:
NameError in CheckinsController#show
undefined local variable or method `checkin’ for #
Rails.root: /my_path/my_checkins
Application Trace | Framework Trace | Full Trace
app/models/checkin.rb:4:in `block in ‘
app/controllers/checkins_controller.rb:17:in `show’
Request
However, I was not able to Debug this error, when I replace the checkin.location and checkin.id against location.location and location.id in the checkin.rb models scope, the error disappears, but the Distance Calculations are always true which is not what we want I guess.
Thx for help,
Mike
Good day,
I have been trying to follow this tutorial using rails 3.2.8, Postgres 9.2.1 and PostGIS 2. and have encountered a number of issues, firstly because, I am new to all 3 and secondly, because they have all changed significantly over the last year.
would you consider updating this tutorial, your help in this regard would be very much appreciated.
Thanks
Duma
You actually make it seem so easy with your presentation but I find this topic to be really something
that I think I would never understand. It seems too complicated and extremely broad for me.
I am looking forward for your next post, I will try to get
the hang of it!
First off I want to say great blog! I had a quick question which I’d like to ask if you don’t mind.
I was curious to know how you center yourself and clear your mind before writing.
I have had a hard time clearing my thoughts in getting my thoughts out.
I truly do enjoy writing but it just seems
like the first 10 to 15 minutes tend to be lost simply just trying to
figure out how to begin. Any ideas or tips? Appreciate it!
I do agree with all the ideas you have introduced on your post.
They’re really convincing and can definitely work. Still, the posts are very quick for novices. May just you please extend them a little from next time? Thanks for the post.
Its such as you learn my thoughts! You seem to grasp so much about this, like you wrote the e-book in it or something.
I feel that you can do with a few percent to drive the message house a bit,
but instead of that, this is fantastic blog. An excellent read.
I will definitely be back.
My blog post: Hermine
[...] 레일즈와 PostGIS 연동은 http://lassebunk.dk/2011/09/10/creating-a-location-aware-website-using-ruby-on-rails-and-postgis/ 을 참고했습니다. Share this:TwitterFacebook This entry was posted in Uncategorized by [...]