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:
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.
The Geocode Search
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>
COPYRIGHT.
Copyright 2006 J. Shirley. This document can be freely redistributed and can be modified and re-distributed under the same conditions as Perl itself.