Ruby on Rails, programming, and SEO
Check out my articles about Ruby on Rails, programming, and SEO. Weekly updates? Subscribe to my feed.

Creating a location aware website using Ruby on Rails and PostGIS

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 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: , , ,

30 comments

  1. Mauro says:

    You may want to checkout https://github.com/dazuma/rgeo ;)

  2. netbooks says:

    Its not my first time to visit this web page, i am browsing this web page dailly and obtain pleasant information from here everyday.

  3. Jose says:

    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.

  4. lassebunk says:

    @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 :/

  5. Jose says:

    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!

  6. lassebunk says:

    @Jose: Glad you like it :)

    Could you tell me the line number and what’s in that line?

    Thanks,
    /Lasse

  7. Jose says:

    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!

  8. lassebunk says:

    Just a guess, but in app/models/checkin.rb, try replacing the two ” = value” with ” = value.to_f”

  9. Jose says:

    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!

  10. Jose says:

    Just in case,

    The marker is showing up in the map correctly.

  11. lassebunk says:

    Which view is that? :)

  12. Jose says:

    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!

  13. lassebunk says:

    Thanks! Just looked at it – you can just replace < %= xx.location %> with < %= xx.location.y %>, < %= xx.location.x %> – then it would work :)

    /Lasse

  14. Jose says:

    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 !

  15. lassebunk says:

    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?

  16. Jose says:

    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.

  17. lassebunk says:

    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

  18. Jose says:

    Sure. no problem. I’ll provide you the link (eventually) :P
    It’s the least I could do !

    Nice to meet you.

    Jose.

  19. lassebunk says:

    Sounds good :)
    Nice to meet you too :)

    /Lasse

  20. ambien says:

    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.

  21. Michael Ruepp says:

    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

  22. Duma Mtungwa says:

    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

  23. Nigel says:

    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!

  24. 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!

  25. 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.

  26. Hermine says:

    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

  27. [...] 레일즈와 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 [...]

  28. Pretty! This was an extremely wonderful article. Thank you for
    providing these details.

    Look into my web page: sessel aufstehhilfe

  29. Haarausfall says:

    Heya i’m for the primary time here. I came across this board and I in finding It really useful & it helped me out much. I’m hoping to provide something again and aid others like you aided me.

    Also visit my page :: Haarausfall

  30. Yo, that was a useful summary! Lots of helpful advise, I am thankful that I found it.

    I am going to save your site! :-)

Leave a comment

 
Fork me on GitHub