Integrating Virtual Earth and Ruby on Rails

This article is written for an old version of the Virtual Earth platform. While still available for reference purposes, it is unlikely to work if implemented.

It's time to once again visit Microsoft's MapSearchControl. This time, we'll be integrating the control into the current darling of the "Web 2.0" movement - Ruby on Rails (aka RoR).

If you are unfamiliar with the basic setup of the MapSearchControl, you must read about our  ASP.NET implementationand our PHP implementation. Our Ruby on Rails implementation will not break any new ground with MapSearchControl, so I will focus more on the Ruby and Rails side.

Ruby, and Ruby on Rails.

Ruby is a brilliant little language initially created by Yukihiro Matsumoto. It has gained a cult following due to its concise and elegant syntax, object orientation, and extensive standard library.

However, Ruby didn't really hit the big time until the Ruby On Rails framework came about. RoR is billed as a "full stack, open source web framework." What this boils down to is that it's a very well put together MVC framework, with a truckload of code generators that enable you to churn out code at an alarming rate.

Getting started with RoR

If you are completely unfamiliar with Ruby and RoR, I would suggest starting with one of the many tutorials available on the RoR documentation website. One of the quickest will be this O'Reilly OnLamp article.

InstantRails is a great way to get started quickly.

The rest of this tutorial will assume that you have, at bare minimum, installed Ruby, RoR, and worked through at least one basic tutorial.

Our Virtual Earth RoR application.

The first thing we must do is create our RoR framework for our Virtual Earth application. In RoR we achieve this by running the command:

rails virtualearth

This creates a directory called 'virtualearth'. The contents of this folder are covered in most basic RoR tutorials; we assume you have a passing familiarity with them.

Overview of controllers and views

Recall that RoR takes HTTP requests and initially maps these to a controller, which is simply a Ruby class. RoR automatically determines what class method must be called for a particular request.

The controller method then has an opportunity to perform business logic and instantiate variables, before RoR passes the request on to the corresponding view. The view is a HTML page with embedded Ruby instructions, and may access certain variables created by the controller.

For our Virtual Earth page, we will need to generate a basic controller and then provide three methods: index, what, and ads. 'index' will serve up our basic HTML page, and 'what' and 'ads' will proxy search requests to MSN as we did in our ASP.NET and PHP examples.

Creating the controller.

To create the empty template for this controller, we must again use RoR's code generator scripts. Note that in this RoR app we have no need to generate a model, as we are not accessing any data source.

We will create a controller simply called 've'. From inside the virtualearth directory, we issue the command:

./script/generate controller ve

Or from within an InstantRails installation:

ruby script\generate controller ve

RoR automatically creates a variety of template files that we can begin to edit. The main controller is located at "app/controllers/ve_controller.rb".

Our RoR controller

Let's dive right in and look at the completed controller. Feel free to cut & paste this code into ve_controller.rb. We'll soon step through this code in more detail to discuss some of Ruby's syntactic idiosyncracies.

class VeController < ApplicationController
public
  
  def index
  end

  def what
    expectedArgs = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'i', 'r' ]
    render :text => queryMsnSearch(expectedArgs, "http://virtualearth.msn.com/search.ashx")
  end

  def ads
    expectedArgs = [ 'a', 'b', 'c', 'd' ]
    render :text => queryMsnSearch(expectedArgs, "http://virtualearth.msn.com/Ads.ashx")
  end

  private
  
    def queryMsnSearch(expectedArgs, searchUrl)
      foundItems = Array.new
      
      uri = URI.parse(searchUrl)

      expectedArgs.each do |arg|
        if @params.has_key?(arg)
          foundItems << "#{arg}=#{@params[arg]}"
        end
      end

      Net::HTTP.start(uri.host, uri.port) do |http|
        response = http.post(uri.path, foundItems.join("&"))
        return response.body
      end
    end
    
  end
  
end

The Index method

We have deliberately defined an empty 'index' method on our class. 'index' will be called as the default page when users access our controller. Since there is no logic behind serving the simple index page, we can go straight to creating the view. In a more complex view, we might access or render some variables instantiated in our controller class method.

The corresponding view for method 'index' on class 've' will be located at "app/views/ve/index.rhtml" relative to the virtualearth directory.

Create this file and edit it. This file will look very similar to our ASP.NET and PHP versions, except for changes to the location of the what and ads pages.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
  <head>
    <title>Sample 1</title>
    <link href="http://dev.virtualearth.net/commercial/v1/VE_MapSearchControl.css" type="text/css" rel="stylesheet"/>
    <script src="http://dev.virtualearth.net/commercial/v1/VE_MapSearchControl.js">
    </script>
  </head>
  <script>
    var map = null;
    function MyOnLoad()
    {
        map = VE_MapSearchControl.Create(
                  47.6, -122.33, 12, 'r', "absolute", 
                  10, 10, 600, 500, 
                  "/ve/what",
                  "/ve/ads");

        document.getElementById("contents").appendChild(map.element);
    }
  </script>
  <body onLoad="MyOnLoad()">
    <div id="contents" style="font-size:10pt">
    </div>
  </body>
</html>

Fire up!

At this point you can actually fire up the RoR built in web server and verify that what we've done works. Issue the following command:

./script/server

Or from within an InstantRails installation:

ruby script\server

Now visit http://localhost:3000/ve/. You should see our familiar Virtual Earth map, and be able to search as per normal.

If you are anything like me, you'll be quite impressed by the succinctness of Ruby and the elegance of the Ruby on Rails framework. Alternatively, you might be perplexed by Ruby's syntax - in which case read on for a more detailed discussion.

About the 'what' and 'ads' methods

While writing this part of the article I got pretty excited about Ruby. I love languages that are terse, but easy to understand. After writing a few methods, I started to guess the required syntax rather than constantly consult the reference documentation - and was very satisfied to find my guesses almost always worked! Ruby truly follows the Principal of Least Surprise - once you start writing Ruby you won't be unpleasantly surprised by interface inconsistency.

def what
  expectedArgs = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'i', 'r' ]
  render :text => queryMsnSearch(expectedArgs, "http://virtualearth.msn.com/search.ashx")
end

def ads
  expectedArgs = [ 'a', 'b', 'c', 'd' ]
  render :text => queryMsnSearch(expectedArgs, "http://virtualearth.msn.com/Ads.ashx")
end

As in our PHP example, we have factored out the guts of these methods into a function called queryMsnSearch. The only thing that changes between the two is the expectedArgs array and the URL which we're querying.

Something to note is that rather than letting the method fall through into a 'view' template as we did with 'index', we have instead rendered some output directly to the browser. In most circumstances you would avoid this as it violates the principal of the MVC, but it makes sense in this scenario as we're just proxying a page.

To read up more on the render method, see the RoR documentation for ActionController::Base.

Creating the queryMsnSearch helper function

private
  def queryMsnSearch(expectedArgs, searchUrl)

  foundItems = Array.new

  uri = URI.parse(searchUrl)

  expectedArgs.each do |arg|
    if @params.has_key?(arg)
      foundItems << "#{arg}=#{@params[arg]}"
    end
  end

  Net::HTTP.start(uri.host, uri.port) do |http|
    response = http.post(uri.path, foundItems.join("&"))
    return response.body
  end
end

Again, this is not dissimilar from how we implemented this routine in PHP. Remember, the purpose of this routine is to pass a series of HTTP arguments on to another URL, but only the ones listed in expectedArgs.

Firstly, note that we're creating queryMsnSearch as a private method of the class. This means that attempts to access /ve/queryMsnSearch via HTTP will fail.

uri = URI.parse(searchUrl)

Ruby is an object oriented language. URI is a standard library object that helps us manipulate URIs. Once we have parsed the URI we can do neat things with it, such as extract the path, the host, and the port.

expectedArgs.each do |arg|
...
end

This is essentially the equivalent of a foreach block. What we're doing here is calling method 'each' on array expectedArgs. As an argument to 'each', we're passing a block, denoted by do ... end. This will called for every element of the array, and inside that method the current array element will be called 'arg'.

'each' is a method that's part of the Array class, which is documented in the Core API documentation

if @params.has_key?(arg)
  foundItems << "#{arg}=#{@params[arg]}"
end

The first line reads pretty easily. It says "if params has a key named arg". Note that '@' in Ruby denotes an instance variable of a class. @params is automatically provided to our class because we're a child of class ActionController. Also note that we can tell has_key can be used in a boolean context because of the trailing '?' in the method signature.

If the test returned true, we push a value on to foundItems. This is done with the << syntax. All we're actually pushing into foundItems is a string. In a Ruby string, the #{something} construct is interpolated to whatever the value of 'something' is. In our case, it's the variable arg, followed by, "=", followed by the value of the matching key inside the hash @params.

Remember that everything is an object in Ruby, so if we did:

foundItems << "#{arg}=#{@params[arg]}".length

We would actually push the length of that string into foundItems instead. Cool.

Net::HTTP.start(uri.host, uri.port) do |http|
  response = http.post(uri.path, foundItems.join("&"))
  return response.body
end

This neat construct uses a few concepts we've already come across, so we won't delve too deeply. We call the Net::HTTP.start method, passing the host and port that we parsed out of the URI earlier.

The 'start' method accepts a block with a single argument, 'http'. Why a block? Because at the end of this block, Net::HTTP will conveniently close the socket on our behalf.

Inside the block we simply call the post method against the URI's path. We also pass the result of foundItems.join, which is a string made from joining the elements of foundItems with the specified character, in this case "&".

The HTTP response is collected, and we return the body to the caller.

In summary

It should be pretty clear from this article that Ruby on Rails is an excellent platform from which you can leverage Virtual Earth. RoR's extensive support for web services and AJAX would make it a piece of cake to pull in a rich data source and start to map it on Virtual Earth.

Article contributed by Luke Burton. Have you got something to contribute?

Copyright 2009. Sponsored by nsquared   |  Terms Of Use  |  Privacy Statement
Content on this site is generated from the developer community and shared freely for your enjoyment and benefit. This site is run independently of Microsoft and does not express Microsoft's views in any way.