Day 23 - Creating a Google Maps Mashup

Create a Catalyst Application that queries Google Geocode Service and uses it to create a searchable map.

Google Maps Mashups

Since Google Maps was made public in July of 2005, hundreds of sites have joined up in combining the mapping API with innovative and helpful applications. Many people are always wondering just how to get the maps on their sites, and this is a good method on how to do so with Catalyst.

Installation Prerequisites

Here's a list of modules and Catalyst plugins used in this module:

Geo::Coder::Google
Catalyst::View::JSON

A note about maps

Any mapping application actually has two parts to managing the data:

Geocoding

Geocoding is the process of turning a human readable address ("1600 Pennsylvania Ave Washington DC") into a coordinate pair to display on the map. We'll cover this by using Catalyst::Model::Geocode.

Map Display

There are several mapping APIs to choose from, the three most popular are:

Google Maps

Google Maps is probably the most popular mapping solution out. It incorporates a well-featured JavaScript API and is a combination of JavaScript and image tiles. We'll be using this, since it has the simplest APIs available.

Yahoo Maps

Yahoo Maps is another very popular mapping service that relies on Flash to display the map, and comes with a JavaScript API to customise.

Microsoft Live

Microsoft Live's mapping service

Application Overview

Our application will simply display a Google Map, run geocoding queries through Catalyst, and run local queries through a JS engine. The purpose of this application is to show how to structure an application that has both client-based data requests via JavaScript (Google Maps), and server-based requests (Geocoding) handled via AJAX (using the Google Maps library for transport).

It is a good combination of Chained actions and static content.

Getting Started

The first step for this application is the same with any Catalyst application, and that is to allow Catalyst to setup the application structure:

    catalyst.pl GeoCat

To access Google's services (the mapping API and the geocoding API) you must register your application with Google. It's very simple to do so, just visit http://www.google.com/apis/maps/signup.html to get an API key, and for the sake of this example we'll use the key provided for an application at "localhost". The key for localhost is:

 ABQIAAAAh3LqwY1XU3ldEkDJBxe4hBT2yXp_ZAY8_ufC3CFXhHIE1NvwkxTz_vK_MxtAQetI990enKf6YEthTA

To get access to this in our application, we'll put it in our applications configuration (currently in YAML format, but in the future Catalyst will use Config::General by default). To keep it simple, we'll simply create a config key called "google" and stick the api key in.

Open up the GeoCat/geocat.yml file, and enter this information:

    ---
    name: GeoCat
    google:
        # This key is good only for http://localhost/
        apikey: "ABQIAAAAh3LqwY1XU3ldEkDJBxe4hBT2yXp_ZAY8_ufC3CFXhHIE1NvwkxTz_vK_MxtAQetI990enKf6YEthTA"
    View::TT:
        TEMPLATE_EXTENSION: '.tt'

Once that is completed, it is time to install all the necessary bits. First up on the install list is Miyagawa's Geo::Coder::Google module and his Catalyst::View::JSON module:

    cpan Geo::Coder::Google 
    cpan Catalyst::View::JSON

Then it is time to create the necessary models and controllers for our application. Catalyst creates the Root controller for us, but we'll need to create an additional controller to handle searching, and a model for it to go along with.

    script/geocat_create.pl controller Search
    script/geocat_create.pl model Geocode

And the views:

    script/geocat_create.pl view TT TT
    script/geocat_create.pl view JSON JSON

Setting the default actions

We're using the Chained Dispatch type (Catalyst::DispatchType::Chained) for our application, which has a different structure than what is typically used but is definitely the way forward for Catalyst applications. The first step is to tell Catalyst exactly where to start the chain.

Root.pm, The Beginning

We'll do this in the Root.pm controller, in GeoCat/lib/GeoCat/Controller/Root.pm. The default handler should always be treated as a "404 Not Found" error, so lets make it do that and create our handlers in Root.pm:

    # 404 Handler
    sub default : Private {
        my ( $self, $c ) = @_;

        $c->response->status(404);
        $c->response->body( qq|404: Not found!| );
    }

    # Base, the starting point
    sub base : Chained('/') : PathPart('') : CaptureArgs(0) {
    }
    # Root, or "/" as the URI is perceived
    sub root : Chained('base') : PathPart('') : Args(0) {
        $c->response->body('Matched GeoCat::Controller::Root in Root.');
    }

Now these don't actually do anything, just inform Catalyst that there will be an execution chain with these actions.

Search.pm

Lets finish the chain by setting up GeoCat/lib/GeoCat/Controller/Search.pm:

    # Anything under /search should bind to this action, and it will always
    # be called
    sub base : Chained('/base') : PathPart('search') : CaptureArgs(0) {
    }

    # What the user gets when they visit "/search" (the root action of Search)
    sub root : Chained('base') : PathPart('') : Args(0) {
        my ( $self, $c ) = @_;
        $c->response->body('Matched GeoCat::Controller::Search in Search.');
    }

Checking our work

After this, it may be a good idea to fire up GeoCat and make sure the actions are defined properly. You should see a section in the debug output that looks like this:

 $ script/geocat_server.pl 
 [ SNIP ]
 [debug] Loaded Chained actions:
 .-------------------------------------+--------------------------------------.
 | Path Spec                           | Private                              |
 +-------------------------------------+--------------------------------------+
 |                                     | /base (0)                            |
 |                                     | => /root                             |
 | /search                             | /base (0)                            |
 |                                     | -> /search/base (0)                  |
 |                                     | => /search/root                      |
 '-------------------------------------+--------------------------------------'

If you see those definitions, the chains are ready to go (and you can visit both http://localhost:3000/ and http://localhost:3000/search to see the output, but for now it is pretty boring.

Creating our templates

GeoCat is a simple application, and as such only has a few templates. Instead of pasting them in the writeup, you can fetch them along with the rest of GeoCat from the Catalyst subversion repository. We're only creating one template because we'll only be displaying one page, and then have the search data returned in JSON format.

root.tt

The root action ("/") will automatically point to root.tt as the template to render. Catalyst figures this out by the final calling action, which can be determined by the => in the "Private" column of the "Loaded Chained actions" output above. In the code, you can simply use $c->action.

To fetch this template, please look here and store the file at GeoCat/root/root.tt:

http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/GeoCat/root/root.tt

Please note the paths for the template to make sure GeoCat knows where to find it.

Defining our work flow

The first root template simply defines our map based upon our API key given by Google.

All we're really doing is proxying the search results from Google's Geocoding service, but it is easy to extend these results to allow other features. There are a couple steps to this. The first is to setup the geocode object, which is done on a per-usage basis, and as such we'll hitch into the ACCEPT_CONTEXT method, partially to access the application config structure, but to also create a Geocoder object if we need to. The code for this is simply:

    sub ACCEPT_CONTEXT {
        my ( $self, $c ) = @_;
        # Create the geocoder object if we need to
        $self->{geocoder} ||= Geo::Coder::Google->new(
            apikey => $c->config->{google}->{apikey} );
        return $self;
    }

Now that the geocoder object itself is created, that means our geocode function is quite small:

    sub geocode { 
        my ( $self, $location ) = @_; 
        return $self->{geocoder}->geocode( location => $location ); 
    }

The method for geocoding is to simply pass this back to the client, since Google returns JSON we will to, by using View::JSON. This is handled in the controller GeoCat::Controller::Search, under the root action we defined earlier:

    sub root : Chained('base') : PathPart('') : Args(0) { 
        my ( $self, $c ) = @_; 
        if ( $c->req->params->{location} ) {
            $c->stash->{location} = $c->model('Geocode')->geocode(
                $c->req->params->{location});
        }
        $c->forward('View::JSON');
    }

Now you can test this by starting the server, and issuing a GET command:

 $ GET
 http://caffei:3005/search?location=1600%20pennsylvania%20ave%washington%20DC
 {"location":{"AddressDetails":{"Country":{"AdministrativeArea":{"SubAdministrativeArea":{"SubAdministrativeAreaName":"District
 of
 Columbia","Locality":{"PostalCode":{"PostalCodeNumber":20004},"LocalityName":"Washington","Thoroughfare":{"ThoroughfareName":"1600
 Pennsylvania Ave
 NW"}}},"AdministrativeAreaName":"DC"},"CountryNameCode":"US"},"Accuracy":8},"address":"1600
 Pennsylvania Ave NW, Washington, DC 20004,
 USA","Point":{"coordinates":[-77.037691,38.898758,0]}}}

All that data means it is working! The important point to note is the "coordinates" field in the result set.

Now, if you'll notice the default action is returning JSON data. This is because, by default, Catalyst will simply pick whichever view comes first unless instructed otherwise. To pick TT as our default view, we simply need to insert the "default_view" key in our configuration file geocat.yml:

    --- name: GeoCat
        default_view: TT

Now when you load GeoCat (make sure you restart the server) you'll get the proper view for each action.

Putting it together

Now, with the templates and the included JavaScript, you should have a functional Google Map search application. The JavaScript in root.tt will query /search?location=UserInput in the search box, and then make requests to the Google Local search API without ever hitting your server.

AUTHOR

J. Shirley, <jshirley@gmail.com>