Thursday, May 21, 2009

Using the Google Static Maps API and HTTP Geocoder to power Lonely Planet's mobile travel guide

This post is part of the Who's @ Google I/O, a series of blog posts that give a closer look at developers who'll be speaking or demoing at Google I/O. Today's post is a guest post written by Ken Hoetmer of Lonely Planet.

Lonely Planet has been using Google Geo APIs since 2006 - you can currently find them in use on destination profiles at lonelyplanet.com , in our trip planner application , in our hotel and hostel booking engine , on lonelyplanet.tv , and in our mobile site, m.lonelyplanet.com . I could talk for hours about any of these sites, but in preparation for Google I/O and my talk at the Maps APIs and Mobile session, I'll spend this post discussing our use of the Google Static Maps API and the HTTP geocoding service on m.lonelyplanet.com.

Our mobile site's primary feature is highlighting points of interest (POIs) around you, as selected by Lonely Planet. The site is browser based and targeted at a baseline of devices. This accessibility is great for on the road, but because of this choice, we can't obtain precise user locations via a location API. Instead, we've asked our users to self-select their location by entering it into a free form text field when they first arrive at the site. This location is then posted to our server, geocoded on the back end by forwarding the text to the Google HTTP geocoding API, and then used to either set the user's location or return a list of options for disambiguation.

Knowing the user's position, we then forward the position and a radius in kilometers to our Content API's POI proximity method, returning a list of points within range, in order of proximity. Once we have the POIs, we need to present them on a map, relative to the user's location. This is where the Google Static Maps API comes in. We can't rely on the availability of Flash, JavaScript, and Ajax, but the Static Maps API enables us to serve a JPEG map by simply providing our list of POI geocodes, a few bits about labeling markers, and a height / width (which we calculate per device by querying screen sizes from WURFL) as query parameters to a URL. Below the map we put a few links for switching the map between (road)map, satellite, hybrid, and terrain base maps.

That gives us a basic map, but what if the user wants to look a little farther to the north or east? To enable this, we augmented the map with a lightweight navigation bar (north, south, east, west, zoom in, zoom out), with links to new static maps that represent a pan or zoom action. Here's how we generated the links:

Let's say our page has a static map of width w pixels, height h pixels, centered at (lat,lng) and zoom level z.
$map_link = "http://maps.google.com/staticmap?key={$key}&size={$w}x{$h}&center={$lat},{$lng}&zoom={$z}";
Then, we can generate north, south, east, and west links as follows (this example assumes the existence of a mercator projection class with standard xToLng, yToLat, latToY, lngToX routines):
// a mercator object
$mercator = new mercator();

// we'll pan 1/2 the map height / width in each go

// y pixel coordinate of center lat
$y = $mercator->latToY($lat);

// subtract (north) or add (south) half the height, then turn
back into a latitude
$north = $mercator->yToLat($y - $h/2, $z);
$south = $mercator->yToLat($y + $h/2, $z);


// x pixel coordinate of center lng
$x = $mercator->lngToX($lng);

// subtract (west) or add (east) half the width, then turn back into a longitude
$east = $mercator->xToLng($x + $w/2, $z);
$west = $mercator->xToLng($x - $w/2, $z);
So that our north, south, east, west links are:
$north = "http://maps.google.com/staticmap?key={$key}&size={$w}x{$h}&center={$north},{$lng}&zoom={$z}";
$south = "http://maps.google.com/staticmap?key={$key}&size={$w}x{$h}&center={$south},{$lng}&zoom={$z}";
$east = "http://maps.google.com/staticmap?key={$key}&size={$w}x{$h}&center={$lat},{$east}&zoom={$z}";
$west = "http://maps.google.com/staticmap?key={$key}&size={$w}x{$h}&center={$lat},{$west}&zoom={$z}";
Of course if you're serving a page knowing only a list of points and their geocodes, then you don't have a zoom level value for calculating the map links. Thankfully, mercator projection implementations often offer a 'getBoundsZoomLevel(bounds)' function, which serves this purpose (create your bounds by finding the minimum and maximum latitudes and longitudes of your list of geocodes). If your implementation doesn't provide this function, it's not complicated to write, but I'll leave that to the reader (hint: find difference in x and y values at various zoom levels and compare these to your map width and height).

As mentioned earlier, I'll be joining Susannah Raub and Aaron Jacobs to delve deeper into maps and mobile devices at Google I/O. In addition, Matthew Cashmore (Lonely Planet Ecosystems Manager) and I will be meeting developers and demoing our apps in the Developer Sandbox on Thursday, May 28. We'd love to meet you and we'll be easy to find - simply follow the noise to the jovial Welshman.

No comments:

Post a Comment