Rails, Geocoding, and Google Maps

Apr 18 by Andre
Update April '07: GeoKit, my Rails Geocoding plugin, abstracts away all the geocoding logic for multiple providers (Google, Yahoo Geocoder.us, Geocoder.ca -- including failover!), distance-based finders for ActiveRecord, and much more!
This is a simple example to demonstrate how to display a Google Map using Ruby on Rails, including utilizing a geocoding web service to translate addresses to geocodes.

Why the lookup web service? The Google Maps API doesn't just let you map an address. You need to supply the Maps API with a longitude and latitude to place a marker on a map (or even to center a map on a city). The geocoding web service translates a valid address into longitude and latitude; you can pass the result to the Maps API.
1. Get yourself a Google Maps API key
Go to http://www.google.com/apis/maps/signup.html to get your API key. You will need to tell them the URL from which you will be utilizing the map API. If you are experimenting on your own localhost, then fill in "http://localhost"; if you are serving your pages through a port (say because you are developing on WEBrick), include the port, i.e., "http://localhost:3000".

Note that when you deploy to an actual URL, you will need a different Maps API key for "http://www.your-url.com"

2. Write a controller to serve this example
Whenever I'm figuring out something new, I like to isolate it and get it working it the simplest possible case first, and then integrate it with the larger app that I'm working on. I keep a controller around called "Sandbox" for just this purpose.

Here's the Sandbox controller:

class SandboxController < ActionController::Base

  def lookup_geocodes
    # your list of places. In a real app, this would come from the database,
    # and have more robust descriptions
    places = [
      {:address=>'555 Irving, San Francisco, CA',:description=>'Irving'},
      {:address=>'1401 Valencia St, San Francisco, CA',:description=>'Valencia'},
      {:address=>'501 Cole St, San Francisco, CA',:description=>'Cole'},
      {:address=>'150 Church st, San Francisco, CA',:description=>'Church'}
     ]

    # this loop will do the geo lookup for each place
    places.each_with_index do |place,index|
      # get the geocode by calling our own get_geocode(address) method
      geocode = get_geocode place[:address]
     
      # geo_code is now a hash with keys :latitude and :longitude
      # place these values back into our "database" (array of hashes)
      place[:latitude]=geocode[:latitude]
      place[:longitude]=geocode[:longitude]   
   
    end
   
    #place the result in the session so we can use it for display
    session[:places] = places
   
    #let the user know the lookup went ok
    render :text=> 'Looked up the geocodes for '+places.length.to_s+
' address and stored the result in the session . . .'
 
  end

  def show_google_map
    # all we're going to do is loop through the @places array on the page
    @places=session[:places]
  end
 
  private
  def get_geocode(address)
    logger.debug 'starting geocoder call for address: '+address
    # this is where we call the geocoding web service
    server = XMLRPC::Client.new2('http://rpc.geocoder.us/service/xmlrpc')
    result = server.call2('geocode', address)
    logger.debug "Geocode call: "+result.inspect
   
    return {:success=> true, :latitude=> result[1][0]['lat'],
:longitude=> result[1][0]['long']}
  end 
 

end


There are two methods: lookup_geocodes and show_google_map. When you run the example, you will hit lookup_geocodes first, which will make a series of calls to an external web service to get the longitude/latitude of each address we've provided. The result will be placed back in the session, so subsequent calls to show_google_maps can utilize the geocode information.

Why separate it out into two steps? The geocode web service is an external resource -- it's slow sometimes, and may be inaccessible. During busy hours, it takes about 10 seconds per call. So, we'd rather to the lookups upfront, and keep the result (longitudes and latitudes) locally for display.

This structure (one lookup page and one render page) is artificial, but it models how a real application would work. Say you are building a classifieds site which shows for-sale items on a map. When users post items, they would enter the address for the item. At this point, the application would utilize the geocoding web service to find the lat/long for that particular address. The latitude and longitude would then be stored in your database along with rest of the info for the item, ready to render on a map at any time.

3. Create the associated view:
Here's the view. Note that there's no view associated with the lookup_geocodes method, because that renders text instead of forwarding to a view.

You need to put your Google Maps key in where it says [KEY_GOES_HERE]

<html>
<head>
  <title>Google maps example</title>
    <!-- This includes the google maps API code.
You need to put your own key here -->
    <script src="http://maps.google.com/maps?file=api&v=2&key=[KEY_GOES_HERE]"
type="text/javascript"></script>

    <script type="text/javascript">
        // helper function to create markers
        function createMarker(point,html) {
            var marker = new GMarker(point);
            GEvent.addListener(marker, "click", function() {
             marker.openInfoWindowHtml(html);
             });
            return marker;
        }
       
        // this is called when the page loads.
        // it initializes the map, and creates each marker
        function initialize() {
            var map = new GMap(document.getElementById("map"));
            map.addControl(new GSmallMapControl());
            map.centerAndZoom(new GPoint(-122.443882, 37.769079), 5);
           
            <%@places.each do |place|%>
            var point = new GPoint(<%=place[:longitude]%>,<%=place[:latitude]%>);
            var marker = createMarker(point,'<div><%=h place[:description]%></div>')
            map.addOverlay(marker);
            <%end%>
        }   
    </script>

</head>
<body onload="initialize()">

Here's the map:
<!-- This is the element in which the map will be displayed. -->
<div id="map" style="width: 450px; height: 350px"></div>

</body>
</html>


4. Try it out!

First, go to http://localhost:3000/sandbox/lookup_geocodes (assuming that's where you put it)
It will take anywhere from a few seconds to minute to lookup the address. When it's done, your browser should display: "Looked up the geocodes for 4 address and stored the result in the session . . ."

And if you look in your development.log, you should see:

starting geocoder call for address: 555 Irving, San Francisco, CA
Geocode call: [true, [{"prefix"=>"", "city"=>"San Francisco", "zip"=>94122, "number"=>555, "type"=>"St", "street"=>"Irving", "lat"=>37.764075, "long"=>-122.463588, "suffix"=>"", "state"=>"CA"}]]
starting geocoder call for address: 1401 Valencia St, San Francisco, CA
Geocode call: [true, [{"prefix"=>"", "city"=>"San Francisco", "zip"=>94110, "number"=>1401, "type"=>"St", "street"=>"Valencia", "lat"=>37.75053, "long"=>-122.420681, "suffix"=>"", "state"=>"CA"}]]
starting geocoder call for address: 501 Cole St, San Francisco, CA
Geocode call: [true, [{"prefix"=>"", "city"=>"San Francisco", "zip"=>94117, "number"=>501, "type"=>"St", "street"=>"Cole", "lat"=>37.770529, "long"=>-122.450182, "suffix"=>"", "state"=>"CA"}]]
starting geocoder call for address: 150 Church st, San Francisco, CA
Geocode call: [true, [{"prefix"=>"", "city"=>"San Francisco", "zip"=>94114, "number"=>150, "type"=>"St", "street"=>"Church", "lat"=>37.768629, "long"=>-122.428782, "suffix"=>"", "state"=>"CA"}]]
Completed in 41.85900 (0 reqs/sec) | Rendering: 0.00000 (0%) | 200 OK


Now, go to http://localhost:3000/sandbox/show_google_map. You should see:


Here's the map:
 


You've got it!
Now that you've got a basic example up and running, you can adapt it to your own needs, and flesh out the areas that need to be made more robust. Examples:
  • the get_geocode method needs error handling -- what if the web service is unavailable?
  • the javascript code is too verbose -- you don't need to render the entire block of code in the loop on the show_google_map view, you could just about a JSON structure and loop though that.
  • the inline "onload" on the body tag should be replaced with an event registration -- the Google API has them built in, or you can use the one in Prototype.js, or my own event listening code.
Resources

TrackBack URL:

Listed below are links to weblogs that reference Rails, Geocoding, and Google Maps:

» Rails, Geocoding, and Google Maps from
This is a simple example to demonstrate how to display a Google Map using Ruby on Rails, including utilizing a geocoding web service to translate addresses to geocodes. [Read More]

» Google Maps programming from Favorites
Web 2.0 Technologies: Rails, Geocoding, and Google Maps... [Read More]

Comments

1

Dave on Apr 19

Great example!!!

Is there a way to change the push pins to a number so you could see the order of the locations?

2

Daniel Schierbeck on Apr 19

In #lookup_geocodes, you can replace this:

geocode = get_geocode place[:address]
place[:latitude]=geocode[:latitude]
place[:longitude]=geocode[:longitude]

with this:

place.merge! get_geocode(place[:address])

thus simplifying the script.

3

Tony Collen on Apr 19

Hey,

I've been working on my own Rails/GMaps integration. This is with PostGIS and ActiveRecord. You can check out my results at my blog: http://weblog.halogenlabs.com/?p=490

4

Ben Reubenstein on Apr 19

Great tutorial. I have a setup that is similar, with input boxes for the users to add addresses. Once in while the Lat/Long needs tweaking for accuracy, but it is pretty amazing. Now if Google starts showing ads on the map...

5

possibly confused on Apr 19

hey, i'm just learning rails and having an issue getting the pages to load.

i've put the html into:

/var/www/localhost/test/app/views/sandbox/index.html

and goto:

http://localhost:3000/sandbox/index.html

I just get these errors...

NoMethodError in Sandbox#index.html

Showing app/views/sandbox/index.html where line #26 raised:

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occured while evaluating nil.each

Extracted source (around line #26):

23: map.addControl(new GSmallMapControl());
24: map.centerAndZoom(new GPoint(-122.443882, 37.769079), 5);
25:
26:
27: var point = new GPoint(,);
28: var marker = createMarker(point,'')
29: map.addOverlay(marker);

RAILS_ROOT: script/../config/..
Application Trace | Framework Trace | Full Trace

#{RAILS_ROOT}/app/views/sandbox/index.html:26

6

Roj Niyogi on Apr 19

There is also the much lesser known Ontok Geocoder web service that should work too (I think):

http://geocoder.ontok.com
http://www.ontok.com/wiki/index.php/Geocode

Have a look!

Roj Niyogi

7

Phil on Apr 19

Hi, guys,

I have lost track of how many geocoders for google maps I've seen: restaurants linked to google, photos linked to google, real estate for sale linked to google, and on and on and on. I thought it was interesting the first few times, but I've lost track of how many online photo sites let you link your photos to google map (zooomr, flickr, smugmug), restaurant reviews (ning), and so on.

Is there something else that can be done? It seems we've got the whole world linked to google maps. Is google maps it?

8

Peter Boyd on Apr 19

Just did the same thing, except with PHP class. Example is here: http://www.paperstreet.com/map/

Interesting how the PHP logic and Rails logice are about the same. Although with the PHP script it stores the geocodes in a database for future reference. This allows them to load faster and you can get 200+ locations on one map without having to look them up each time.

PHP class - http://www.phpinsider.com/php/code/GoogleMapAPI/

9

SPP on Apr 28

Peter Boyd,

It is interesting you suggest that persisting data to a database in PHP is easier than with ActiveRecord, but I suspect you haven't used Rails before!? I have used both PHP and Rails and, hands down, prefer Rails in every area (model, view and controller).

Thanks for your thoughts and PHP spam link though! Actually this is directed at all the sour PHP coders that keep spamming Rails blog/article entry comments.

-SPP

10

sausheong on May 04

Hi,

I did a similiar step-by-step tutorial for a mashup with Rails using Google Maps, Indeed (job search) and Yahoo web search service. Check it out at http://saush.com/blog/?page_id=48

11

Mike on Jul 06

I have a quick question:

Where' are you getting these cooridinates from?

map.centerAndZoom(new GPoint(-122.443882, 37.769079), 5);

The reason I ask is, I'm not getting the right location in the map. Thanks.

12

Andre on Jul 09

Mike, I originally got the coordinates from mapbuilder.net. Those same numbers are used on this page to render the example map -- view the source to see; the GMaps code is right at the top in the header.

13

Jim C on Aug 29

Would I be wrong to assume that there isn't a reason to do it this way, now that Google Maps v2 has its own geocoder?

I'm not trying to be snooty; I'm sincerely curious.

14

Andre on Aug 29

Jim, you'll still need to go through the step of geocoding your addresses to place them on the map, regardless of whose geocoder you use. In practice, using the Google geocoder or Geocoding.us geococoder is nearly identical -- they're both just REST calls, and parsing some XML or JSON data that comes back.

The Google geocoder DOES give you the extra flexibility of calling the geocoder directly from javascript. It's a nice option to have, and valid for some uses cases. It lots of applications, though, you'll want to do the geocoding call server-side and store the lat/lang values in the database.

15

Nits on Sep 12

I have just started learning Rails. And while doing this project, I had an error.
getaddrinfo: No address associated with hostname

Can someone help me with this?

Thanks in advanace

16

Jason Belec on Jan 19

Thanks, this was great and very educational. I'm still only about 4 months into RoR and this really helped me try something larger.

Had some issues at first, until I realized that Google had updated, so the line:

Had to be altered to:

src="http://maps.google.com/maps?file=api&v=2&key=KEY_GOES_HERE"

Now the page draws the map without error.

I am curious about anywhere I can also see a means of putting the found lat and lng for each row into the database with the proper row and then calling back later as needed.

Thank you.

17

Nick on Jul 03

So how does one go from a lat-lng point to a street address in Rails?

Any help?

Thanks,

Nick

18

Ravi Vardhan on Aug 28

Hi,

I have integrated above code with my rails application,got the error like "get info address"(socket Error) please help me to develop this application

and i want to search any city (like google exp)what can i do for this

19

amit on Sep 19

hi everbody,

i have integrated the above code in my application ,but it gives me error

Bad file descriptor - connect(2)

can anybody help me rectifying this.

20

Rohit on Oct 04

hi everbody,

i have integrated the above code in my application ,but it gives me error

Bad file descriptor - connect(2)

can anybody help me rectifying this.

plzz help me........

21

serkan on Dec 02

function initialize() {
var map = new GMap(document.getElementById("map"));
map.addControl(new GSmallMapControl());
map.centerAndZoom(new GPoint(-122.443882, 37.769079), 5);


var point = new GPoint(,);
var marker = createMarker(point,'')
map.addOverlay(marker);


The rails script doesnt work within the tags...Why?

22

Haris Gulzar on Jan 08

Hello everyone!

I have a question. I have to consume a webservice that will return XML in the format







Can anyone please direct me to a step by step tutorial on how to get this done, and how to retrieve values of address, name and id etc. Please reply at harisgulzar@gmail.com

23

Knut on Feb 23

Hi,
I tried the GeoKIT plugin and the example to decode addresses in Germany, but GEOKit always response Nil:
"You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]"

When I try addresses from the US everything is fine.

Does somebody has an idea how this could be solved or in which format (currently I use 'Street, City, Country') I have to insert such addresses?

24

kirk on Mar 07

Very nice! This site has some interesting tips as well.
http://www.newwebplatform.com/tips-and-tutorials/Google-Maps

25

Thuc Nguyen on Apr 02

I just get there error

Showing sanbox/show_google_map.rhtml where line #26 raised:

You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.each

Extracted source (around line #26):

23: map.addControl(new GSmallMapControl());
24: map.centerAndZoom(new GPoint(-122.443882, 37.769079), 5);
25:
26:
27: var point = new GPoint(,);
28: var marker = createMarker(point,'')
29: map.addOverlay(marker);

help me

Thanks

26

adam on Apr 02

your articles are awesome! thanks
all the code is a little hard to read on yellow though, although its clearly legible i recon its easier to understand when its on black/dark grey or white/light grey as this is what most of code on

27

Chirantan Rajhans on Jun 24

Hi,

I am getting following error even after using the exact code give above (Of course with my own key)

Errno::ETIMEDOUT in SandboxController#lookup_geocodes

A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond. - connect(2)

What is this supposed to mean?

The backtrace of the error is

E:/ruby/lib/ruby/1.8/net/http.rb:564:in `initialize'
E:/ruby/lib/ruby/1.8/net/http.rb:564:in `open'
E:/ruby/lib/ruby/1.8/net/http.rb:564:in `connect'
E:/ruby/lib/ruby/1.8/timeout.rb:56:in `timeout'
E:/ruby/lib/ruby/1.8/timeout.rb:76:in `timeout'
E:/ruby/lib/ruby/1.8/net/http.rb:564:in `connect'
E:/ruby/lib/ruby/1.8/net/http.rb:557:in `do_start'
E:/ruby/lib/ruby/1.8/net/http.rb:546:in `start'
E:/ruby/lib/ruby/1.8/net/http.rb:1044:in `request'
E:/ruby/lib/ruby/1.8/net/http.rb:1001:in `post2'
E:/ruby/lib/ruby/1.8/xmlrpc/client.rb:535:in `do_rpc'
E:/ruby/lib/ruby/1.8/xmlrpc/client.rb:420:in `call2'
#{RAILS_ROOT}/app/controllers/sandbox_controller.rb:39:in `get_geocode'
#{RAILS_ROOT}/app/controllers/sandbox_controller.rb:12:in `lookup_geocodes'
E:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/inflector.rb:250:in `each_with_index'
#{RAILS_ROOT}/app/controllers/sandbox_controller.rb:10:in `each'
#{RAILS_ROOT}/app/controllers/sandbox_controller.rb:10:in `each_with_index'
#{RAILS_ROOT}/app/controllers/sandbox_controller.rb:10:in `lookup_geocodes'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `send'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:619:in `perform_action_without_benchmark'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue'
E:/ruby/lib/ruby/1.8/benchmark.rb:293:in `measure'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/benchmarking.rb:66:in `perform_action_without_rescue'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/rescue.rb:83:in `perform_action'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `send'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:430:in `process_without_filters'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:624:in `process_without_session_management_support'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/session_management.rb:114:in `process'
E:/ruby/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:330:in `process'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/dispatcher.rb:41:in `dispatch'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:113:in `handle_dispatch'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:79:in `service'
E:/ruby/lib/ruby/1.8/webrick/httpserver.rb:104:in `service'
E:/ruby/lib/ruby/1.8/webrick/httpserver.rb:65:in `run'
E:/ruby/lib/ruby/1.8/webrick/server.rb:173:in `start_thread'
E:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start'
E:/ruby/lib/ruby/1.8/webrick/server.rb:162:in `start_thread'
E:/ruby/lib/ruby/1.8/webrick/server.rb:95:in `start'
E:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `each'
E:/ruby/lib/ruby/1.8/webrick/server.rb:92:in `start'
E:/ruby/lib/ruby/1.8/webrick/server.rb:23:in `start'
E:/ruby/lib/ruby/1.8/webrick/server.rb:82:in `start'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/webrick_server.rb:63:in `dispatch'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/servers/webrick.rb:59
E:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
E:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
E:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require'
E:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:342:in `new_constants_in'
E:/ruby/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support/dependencies.rb:495:in `require'
E:/ruby/lib/ruby/gems/1.8/gems/rails-1.2.3/lib/commands/server.rb:39
E:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
E:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
script/server:3

Where am I going wrong?

Please help me out.

Thanks,
Chirantan

28

John Small on Jul 03

I'm integrating this into our site, the kit works very well but I'd like to know if you're planning to incorporate MySQL spatial indexing as that would make lookups on large data sets much faster.

29

JON(CJ) on Jul 23

Classic Error handling. Anyways i am getting screwed up debugging the errors I am getting.

30

Preeti on Sep 23

Works very nice! Thanks so much!

31

Rob on Jan 14

You need to add the line:

require 'xmlrpc/client'

to the get_geocode definition.


Works now for me.
Thanks.

32

Mukesh Sahu on Feb 19

Two modification-

1. Replace "session[:places]" with "session[:str]" in your code.
2. Add require 'xmlrpc/client' at the top of "SandboxController".

Post a comment

 
This is so filters can reject the spam-bots. Thanks!