Ruby on Rails Web Mashup Projects

October 6, 2009

Ruby

«»

Creating a new Rails project

This is the easiest part:


$rails Chapter2

This will create a new blank Rails project.

Installing the Rails plugins that will use the various mashup APIs

In this mashup plugin we’ll need to use GeoKit, a Ruby geocoding library created by
Bill Eisenhauer and Andre Lewis, and YM4R/GM—a Ruby Google Maps mapping
API created by Guilhem Vellut. Install them according to the instructions given in
the section above.

Next, we need to create the database that we will be using.

Configuring database access and creating the database

Assuming that you already know how database migration works in Rails, generate a
migration using the migration generator:


$./script/generate migration create_kiosks

This will create a file 001_create_kiosks.rb file in the RAILS_ROOT/db/migrate
folder. Ensure the file has the following information:


	class CreateKiosks < ActiveRecord::Migration
		def self.up
			create_table :kiosks do |t|
				t.column :name, :string
				t.column :street, :string
				t.column :city, :string
				t.column :state, :string
				t.column :zipcode, :string
				t.column :lng, :float
				t.column :lat, :float
			end
		end
		def self.down
			drop_table :kiosks
		end
	end

GeoKit specifies that the two columns must be named lat and lng. These two
columns are critical to calculating the closest kiosks to a specific location.

Now that you have the migration script, run it to create the Kiosk table in your
RAILS_ROOT folder:

Now that you have the migration script, run migrate to create the Kiosk table in your
RAILS_ROOT folder:


$rake db:migrate

This should create the database and populate the kiosks table with a set of data. If it
doesn't work please check if you have created a database schema with your favorite
relational database. The database schema should be named chapter2_development.
If this name displeases you somehow, you can change it in the RAILS_ROOT/config/
database.yml file.

Creating scaffolding for the project

You should have the tables and data set up by now so the next step is to
create a simple scaffold for the project. Run the following in your
RAILS_ROOT folder:


$./script/generate scaffold Kiosk

This will generate the Kiosk controller and views as well as the Kiosk model.
This is the data model for Kiosk, in the kiosk.rb file. This is found in
RAILS_ROOT/app/models/.


	class Kiosk < ActiveRecord::Base
		def address
			"#{self.street}, #{self.city}, #{self.state}, #{self.zipcode}"
		end
	end

Just add in the address convenience method to have quick access to the full address
of the kiosk. This will be used later for the display in the info box.

Populating kiosk locations with longitude and latitude information

Before we begin geolocating the kiosks, we need to put physical addresses to them.
We need to put in the street, city, state, and zipcode information for each of the
kiosks. After this, we will need to geolocate them and add their longitude and
latitude information. This information is the crux of the entire plugin as it allows you
to find the closest kiosks.

In addition you will need to modify the kiosk creation screens to add in the
longitude and latitude information when the database entry is created.

Populate the database with sample data

In the source code bundle you will find a migration file named 002_populate_
kiosks.rb that will populate some test data (admittedly less than 500 kiosks) into
the system. We will use this data to test our plugin. Place the file in RAILS_ROOT/db/
migrate and then run:


$rake db:migrate

Alternatively you can have some fun entering your own kiosk addresses into the
database directly, or find a nice list of addresses you can use to populate the database
by any other means.

Note that we need to create the static scaffold first before populating the database
using the migration script above. This is because the migration script uses the Kiosk
class to create the records in the database. You should realize by now that migration
scripts are also Ruby scripts.

Bulk adding of longitude and latitude

One of the very useful tools in Ruby, also used frequently in Rails, is rake. Rake is
a simple make utility with rake scripts that are entirely written in Ruby. Rails has a
number of rake scripts distributed along with its installation, which you can find out
using this command:


$rake --tasks

Rails rake tasks are very useful because you can access the Rails environment,
including libraries and ActiveRecord objects directly in the rake script. You can
create your own customized rake task by putting your rake script into the
RAILS_ROOT/lib/tasks folder.

We will use rake to add longitude and latitude information to the kiosks records that
are already created in the database.

Create an add_kiosk_coordinates.rake file with the following code:


	namespace :Chapter2 do
		desc 'Update kiosks with longitude and latitude information'
		task :add_kiosk_coordinates => :environment do
			include GeoKit::Geocoders
			
			kiosks = Kiosk.find(:all)
			begin
				kiosks.each { |kiosk|
				loc = MultiGeocoder.geocode(kiosk.address)

				kiosk.lat = loc.lat
				kiosk.lng = loc.lng
				kiosk.update
				puts "updated kiosk #{kiosk.name} #{kiosk.address} =>
										[#{loc.lat}, #{loc.lng}]"
							}
				rescue
				puts $!
			end
		end
	end

In this rake script you first include the Geocoders module that is the main tool for
discovering the coordinate information. Then for each kiosk, you find its longitude
and latitude and update the kiosk record.

Run the script from the console in the RAILS_ROOT folder:


$rake Chapter2:add_kiosk_coordinates

Depending on your network connection (running this rake script will of course
require you to be connected to the Internet) it might take some time. Run it over a
long lunch break or overnight and check the next day to make sure all records have
a longitude and latitude entry. This should provide your mashup with the longitude
and latitude coordinates of each kiosk. However your mileage may differ depending
on the location of the kiosk and the ability of the geocoding API to derive the
coordinates from the addresses.

Adding longitude and latitude during kiosk creation entry

Assuming that you have a kiosks_controller.rb already in place (it would be
generated automatically along with the rest of the scaffolding), you need to add in a
few lines very similar to the ones above to allow the kiosk created to have longitude
and latitude information.

First, include the geocoders by adding GeoKit after the controller definition, in
kiosks_controller.rb.


	class KiosksController < ApplicationController
	include GeoKit::Geocoders

Next, add in the highlighted lines in the create method of the controller.


	def create
		@kiosk = Kiosk.new(params[:kiosk])
		loc = MultiGeocoder.geocode(@kiosk.address)
		@kiosk.lat = loc.lat
		@kiosk.lng = loc.lng
		
		if @kiosk.save
			flash[:notice] = 'Kiosk was successfully created.'
			redirect_to :action => 'list'
		else
			render :action => 'new'
		end
	end

Finally, modify the update method in the controller to update the correct longitude
and latitude information if the kiosk location changes.


	def update
		@kiosk = Kiosk.find(params[:id])
		address = "#{params[:kiosk][:street]}, #{params[:kiosk][:city]},
				#{params[:kiosk][:state]}"
		loc = MultiGeocoder.geocode(address)
		params[:kiosk][:lat] = loc.lat
		params[:kiosk][:lng] = loc.lng
		if @kiosk.update_attributes(params[:kiosk])
			flash[:notice] = 'Kiosk was successfully updated.'
			redirect_to :action => 'show', :id => @kiosk
		else
			render :action => 'edit'
		end
	end

Creating the find closest feature

Now that you have the kiosk data ready, it's time to go down to the meat of the code.
What you'll be creating is a search page. This page will have a text field for the user
to enter the location from which a number of kiosks closest to it will be displayed.
However, to be user-friendly, the initial location of the user is guessed and displayed
on the text field.

Create a search action in your controller (called search.rhtml, and place it in
RAILS_ROOT/app/views/kiosks/) to find your current location from the IP address
retrieved from your user.


	def search
		loc = IpGeocoder.geocode(request.remote_ip)
		@location = []
		@location << loc.street_address << loc.city << loc.country_code
	end

The remote_ip method of the Rails-provided request object returns the originating
IP address, which is used by GeoKit to guess the location from Hostip.info. The
location is then used by search.rhtml to display the guessed location.

Note that if you're running this locally, i.e. if you are browsing the application
from your PC to a locally running server (for example, off your PC as well), you
will not get anything. To overcome this, you can use a dynamic DNS service to
point an Internet domain name to the public IP address that is assigned to your PC
by your ISP. You will usually need to install a small application on your PC that
will automatically update the DNS entry whenever your ISP-assigned IP address
changes. There are many freely available dynamic DNS services on the Internet.

When accessing this application, use the hostname given by the dynamic DNS
service instead of using localhost. Remember that if you're running through an
internal firewall you need to open up the port you're starting up your server with. If
you have a router to your ISP you might need to allow port forwarding.

This is a technique you will use subsequently in Chapters 5 and 6.

Create a search.rhtml file and place it in the RAILS_ROOT/app/view/kiosks folder
with the following code:


	<h1>Enter source location</h1>
	Enter a source location and a radius to search for the closest kiosk.
	<% form_tag :action => 'find_closest' do %>
	<%= text_field_tag 'location', @location.compact.join(',') %>
	<%= select_tag 'radius', options_for_select({'5 miles' => 5, '10
	miles' => 10, '15 miles' => 15}, 5) %>
	<%= submit_tag 'find' %>
	<% end %>

Here you're asking for the kiosks closest to a specific location that are within a certain
mile radius. We will be using this information later on to limit the search radius.

After that, mix-in the ActsAsMappable module into the Kiosk model in kiosk.rb.


	class Kiosk < ActiveRecord::Base
		acts_as_mappable
	end

This will add in a calculated column called (by default) distance, which you
can use in your condition and order options. One thing to note here is that the
ActsAsMappable module uses database-specific code for some of its functions, which
are only available in MySQL and PostgresSQL.

Next, create the find_closest action to determine the location of nearest kiosks.


	def find_closest
		@location = MultiGeocoder.geocode(params[:location])
		if @location.success
			@kiosks = Kiosk.find(:all,
		 :o rigin => [@location.lat, @location.lng],
			:conditions => "distance < #{params[:radius]}",
		 :o rder=>'distance')
		end
	end

The ActsAsMappable module mixed in also overrides the find method to include
an originating location, either based on a geocode-able string or a 2-element array
containing the longitude/latitude information. The returned result is a collection of
kiosks that are found with the given parameters.

Finally create a simple find_closest.rhtml view template (and place it in the
RAILS_ROOT/app/view/kiosks/ folder) to display the kiosks that are retrieved.
We'll add in the complex stuff later on.


	<h1><%= h @kiosks.size %> kiosks found within your search radius</h1>
	<ol>
	<% @kiosks.each do |kiosk| %>
	<li><%= kiosk.name%><br/></li>
	<% end %>
	</ol>

Do a quick trial run and see if it works.


	$./script/server

Then go to http://localhost:3000/kiosks/search. If you have some data, put in
a nearby location (e.g. from our source data: San Francisco) and click on 'find'. You
should be able to retrieve some nearby kiosks.

Displaying kiosks on Google Maps

Now that you know where the kiosks are located, it's time to show them on Google
Maps. For this we'll be using the YM4R/GM plugin. If you haven't installed this
plugin yet, it's time to go back and install it.

To add display to Google Maps, you will need to change the find_closest action as
well as the find_closest view template. First, add the find_closest action in the
kiosks_controller.rb:


	def find_closest
		@location = MultiGeocoder.geocode(params[:location])
		if @location.success
			@kiosks = Kiosk.find(:all,
			 :o rigin => [@location.lat, @location.lng],
				:conditions => ["distance < ?", params[:radius]],
			 :o rder=>'distance')
			@map = GMap.new("map_div")
			@map.control_init(:large_map => true, :map_type => true)
			# create marker for the source location
			@map.icon_global_init( GIcon.new(:image =>
					"http://www.google.com/mapfiles/ms/icons/red-pushpin.png",
								:shadow => "http://www.google.com/
											mapfiles/shadow50.png",
								:icon_size => GSize.new(32,32),
								:shadow_size => GSize.new(37,32),
								:icon_anchor => GPoint.new(9,32),
								:info_window_anchor => GPoint.new(9,2),
								:info_shadow_anchor =>
											GPoint.new(18,25)),
					"icon_source")
			icon_source = Variable.new("icon_source")
			source = GMarker.new([@location.lat, @location.lng],
						:title => 'Source',
						:info_window => "You searched for kiosks
							<br>#{params[:radius]} miles around this source",
						:icon => icon_source)
			@map.overlay_init(source)
			# create markers one for each location found
			markers = []
			@kiosks.each { |kiosk|
				info = <<EOS
		<em>#{kiosk.name}</em><br/>
		#{kiosk.distance_from(@location).round} miles away<br/>
		<a href="http://maps.google.com/maps?saddr=#{u(@location.to_
		geocodeable_s)}&daddr=#{u(kiosk.address)}>directions here from
		source</a>
		EOS
				markers << GMarker.new([kiosk.lat, kiosk.lng], :title =>
		kiosk.name, :info_window => info)
			}
			@map.overlay_global_init(GMarkerGroup.new(true, markers),"kiosk_
		markers")
			# zoom to the source
			@map.center_zoom_init([@location.lat, @location.lng], 12)
		end
	end

Google Maps API is a JavaScript library and YM4R/GM code is a library that
creates JavaScript scripts to interact and manipulate the Google Maps API. Almost
all classes in the library correspond with an equivalent Google Maps API class, so
it is important that you are also familiar with the Google Maps API. The online
documentation comes in very useful here so you might want to open up the
Google Maps reference documentation (http://www.google.com/apis/maps/
documentation/reference.html) as you are coding.

Let's go over the code closely.

The first line creates a GMap object that is placed inside a

tag with the id
map_div while the second line sets some control options.


	@map = GMap.new("map_div")
	@map.control_init(:large_map => true, :map_type => true)

The next few lines then create a GMarker object from the source location that the
user entered that uses a specific icon to show it then overlays it on the map. There
are several options you can play around with here involving setting the image to
be shown as the marker. For this chapter I used a red-colored pushpin from Google
Maps itself but you can use any image instead. You can also set the text information
window that is displayed when you click on the marker. The text can be in HTML so
you can add in other information including images, formatting, and so on.


	# create marker for the source location
	@map.icon_global_init( GIcon.new(:image =>
			"http://www.google.com/mapfiles/ms/icons/red-pushpin.png",
							:shadow => "http://www.google.com/
									mapfiles/shadow50.png",
							:icon_size => GSize.new(32,32),
							:shadow_size => GSize.new(37,32),
							:icon_anchor => GPoint.new(9,32),
							:info_window_anchor => GPoint.new(9,2),
							:info_shadow_anchor =>
							GPoint.new(18,25)), "icon_source")
	icon_source = Variable.new("icon_source")
	source = GMarker.new([@location.lat, @location.lng],
				:title => 'Source',
				:info_window => "You searched for kiosks
					<br>#{params[:radius]} miles around this source",
				:icon => icon_source)
	@map.overlay_init(source)

The lines of code after that go through each of the located kiosks and create a
GMarker object then overlay it on the map too. For each kiosk location, we put in an
info window that describes the distance away from the source location and a link
that shows the directions to get from the source to this kiosk. This link goes back
to Google and will provide the user with instructions to navigate from the source
location to the marked location.

Note that you need to URL encode the location/address strings of the source and
kiosks, so you need to include ERB::Util as well (along with GeoKit::Geocoders).
This is the u() method. In kiosks_controller.rb,add:


	include ERB::Util

then add the following (beneath the code entered above):


			# create markers one for each location found
			markers = []
			@kiosks.each
			{ |kiosk|
				info = <<EOS
	<em>#{kiosk.name}</em><br/>
	#{kiosk.distance_from(@location).round} miles away<br/>
	<a href="http://maps.google.com/maps?saddr=#{u(@location.
	to_geocodeable_s)}&daddr=#{u(kiosk.address)}>directions here from
	source</a>
	EOS
			markers << GMarker.new([kiosk.lat, kiosk.lng],
					:title => kiosk.name, :info_window => info)
		}
		@map.overlay_global_init(GMarkerGroup.new(true, markers),
									"kiosk_markers")

Finally the last line zooms in and centers on the source location.


	# zoom to the source
	@map.center_zoom_init([@location.lat, @location.lng], 12)

Now let's look at how the view template is modified to display Google Maps. The
bulk of the work has already been done by YM4R/GM so you need only to include a
few lines.


	<h1><%= h @kiosks.size %> kiosks found within your search radius</h1>
	<ol>
	<% @kiosks.each do |kiosk| %>
	<li><%= kiosk.name%><br/></li>
	<% end %>
	</ol>
	<%= GMap.header %>
	<%= javascript_include_tag("markerGroup") %>
	<%= @map.to_html%>
	<%= @map.div(:width => 500, :height => 450)%>

Gmap.header creates the header information for the map, including YM4R/GM and
Google Maps API JavaScript files. We are also using GMarkerGroups so we need to
include the GMarkerGroup JavaScript libraries. Next, we need to initialize the map
by calling map.to_html. Finally we'll need to have a div tag that is the same as the
one passed to the GMap constructor in the controller (map_div). This is done by
calling the div method of the GMap object. To size the map correctly we will also need
to pass on its dimensions (height and width here).

And you're ready to roll! Although the page doesn't display the best layout,
you can spice things up by adding the necessary stylesheets to make the view
more presentable.

Summary

What we've learned in this chapter is to create a mashup with Ruby on Rails on a
number of mapping and geocoding providers including Yahoo, Google, geocoder.
us, geocoder.ca, and hostip.info. We learned to create a mashup that gives us a map
of the closest kiosks to a particular location, given an existing database of kiosks
that have location addresses. This is just an introduction to the synergistic value that
mashups bring to the table, creating value that was not available in individual APIs.
When they are all put together, you have a useful feature for your website.

email

«»

Comments

comments