Featured Post

How to process multiple zip-files with Ruby on Rails

Sometimes you have to integrate exported data from third party desktop software. One way to do is to pack all data into a zipped file and push it via FTP onto your server. This tutorial will explain how to process zip-files with your Ruby on Rails™ application. I will take an example from a real estate...

Read More

Create Your Own Geodatabase using Ruby on Rails and Google Maps

Posted by Lars | Posted in Geocoding, Rails, Ruby, Tutorials | Posted on 04-05-2009

6

Geocoding with Ruby on Rails isn’t a big deal. But sometimes you have do more than simple geocoding. For real estate webpages you need information about state, county or suburb for a given address.

This tutorial will show you how to build your own geodatabase without overusing google maps requests(limited on 15000 Requests per Ip/month).

Before you can start you must have a fresh rails application and a google maps api key. You can signup for one here: http://code.google.com/apis/maps/

This tutorial has following parts:

  1. Geocode an address
  2. Get extented information to this address
  3. An Pratical example

We touch only the model layer(thin controllers fat models). I will use an address ressource like follows:

script/generate scaffold Address country:string state:string
county:string city:string suburb:string zipcode:string street:string
streetno:string longitude:float latitude:float error_code:string

and

rake db:migrate

You can use this address ressources for many purposes. But more on this later. You need some ruby-gems too.

1
2
3
4
5
6
7
class Address < ActiveRecord::Base
  require 'open-uri'
  require 'cgi'
  require 'hpricot'
  require 'iconv'
  ...
end

Geocode an address

For geocoding an address you can use a simple callback function.

1
2
3
def before_create
  geocode
end

And the gecode function itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def geocode
  url = ["http://maps.google.com/maps/geo?q="]
  url << address_for_geocoding
  url << "&output=xml&key="
  url << "your_google_maps_api_key"
    open(url.to_s) do |file|
      @body = file.read
      doc = Hpricot(@body)
     
      (doc/:status/:code).each do |code|
        self.error_code = code.inner_html
      end
                                               
      (doc/:point/:coordinates).each do |link|
        self.longitude, self.latitude = link.inner_html.split(',')
      end    
    end  
end

On line 3 i need an encoded address for proper geocoding. This function catches incomplete address information too.

1
2
3
4
5
6
7
8
def address_for_geocoding
  addressline = []     
  addressline << streetno.to_i unless (streetno || "").blank?
  addressline << street unless (street || "").blank?
  addressline << zipcode unless (zipcode || "").blank?
  addressline << city unless (city || "").blank?       
  CGI::escape(addressline.join(' '))
end

On line 8 we start the parsing of the returned xml-data from google maps. For more information about the xml structure you can read: Google Maps XML Structure

On line 15 we assign the longitude and latitude to our address attributes. So you get the coordinates into address model before rails creates it.

Get extented information

You can read at Google Maps XML Structure that far more information can delivered from google maps. So we update our geocode function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def geocode
  url = ["http://maps.google.com/maps/geo?q="]
  url << address_for_geocoding
  url << "&output=xml&key="
  url << "your_google_maps_api_key"
    open(url.to_s) do |file|
      @body = file.read
      doc = Hpricot(@body)
     
      (doc/:status/:code).each do |code|
        self.error_code = code.inner_html
      end

      (doc/:country/:countrynamecode).each do |country_code|
        self.country = country_code.inner_html
      end

      (doc/:country/:administrativearea/:administrativeareaname).each do |area|
        self.state = Iconv.conv('utf-8', 'ISO-8859-1', area.inner_html)
      end
           
      (doc/:country/:administrativearea/:subadministrativearea/:subadministrativeareaname).each do |sub_area|
        self.county = Iconv.conv('utf-8', 'ISO-8859-1', sub_area.inner_html)
      end
           
      (doc/:country/:administrativearea/:subadministrativearea/:locality/:dependentlocality/:dependentlocalityname).each do |t_area|
        self.suburb = Iconv.conv('utf-8', 'ISO-8859-1', t_area.inner_html)
      end
                                               
      (doc/:point/:coordinates).each do |link|
        self.longitude, self.latitude = link.inner_html.split(',')
      end    
    end  
end

So we get extented information about country, state, county and suburb related to the address information you entered. You have now the complete set of address, administrative-level information and geo coordinates.

A practical example

It’s time for some practical stuff. I will use real estate example. In this example i will geocode a house location and add administrative information to it.

So let’s start with a property ressource scaffold:

script/generate scaffold Property title:string description:text
street:string streetno:string zip:string city:string address_id:integer

and

rake db:migrate

I know there are some redundant address information, but it’s necessary to trace typing errors from users.

Lets go to the model layer an make some associations. One house can only have one address but one address can have many houses. So we write:

1
2
3
4
class Address < ActiveRecord::Base
  has_many :properties
  ...
end

and

1
2
3
4
class Property < ActiveRecord::Base
  belongs_to :address
  ...
end

Now lets start coding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Property < ActiveRecord::Base
  belongs_to :address
 
  after_save :get_location

  protected  

  def get_location
    address = address_exist?
    address =Address.create(:streetno =>self.streetno,
     :street => self.street,:zip => self.zip,
     :city => self.city) unless address
    self.address = address
  end

  def address_exist?
    address = Address.find(:first,
    :conditions => "streetno = ? and street= ?
    and zip = ? and city = ?"
,
    self.streetno,self.street,self.zip,self.city)
    address ? address : false
  end
 
end

Done. What happened? If you enter property data the property will saved. After save we call the get_location function to assign a address to our new property.

On line 7 we call the address_exist? function which try to find an address in our database. If this address doesn’t exist we create a new one on line 8.

Everytime a user adds unknown property your address database will grow. Once an address is saved it will resused every time a property with same address is entered.

Now you can get property listings like:

1
2
3
4
5
6
7
8
9
10
class Property < ActiveRecord::Base
  belongs_to :address
  ...
  def get_all_properties_with_same_streetname
    addresses = Address.find(:all,
    :conditions => ["street = ?",self.street])
    addresses.collect { |address| address.properties }
  end
  ...
end

I hope i could help you with this tutorial. Please write a comment if you have any questions or critics.

delicious | digg | reddit | facebook | technorati | stumbleupon | chatintamil

Comments (6)

Nice tutorial.

I am looking for a tutorial for generating tracks from GPX files and showing them using google maps in ruby on Rails.

Any1 who know how to do this in a simple way?

/MartOn

Hi marton,

thanks for your comment. In one of my next tutorials i will explain how to import data from xml files into rails applications.

It isn’t exactly the same but i am sure you can solve your problem with it.

See you later,
Lars

Thanks for the tuturial! It works here!

Only you must change this

addressline << zip unless (zip || “”).blank?

into

addressline << zipcode unless (zipcode || “”).blank?

If i do that it works otherwise it gives an error

Thank you bart, i fixed the error in tutorial.

Kind regards,
Lars

da best. Keep it going! Thank you

Hey,

I really dig the article, very helpful.

Maybe you can help with the next part, I’m looking to parse an address from a block of text (think Craigslist). Any idea how one would go about that? Seems like some serious regex.

Write a comment