Using the Google Maps APIs from App Engine
In a previous post, we discussed how Mapvelopes uses the ReportLab toolkit to dynamically generate PDFs. The other major component of Mapvelopes is its interaction with the various Google Maps APIs, and that's what we'll cover now.
The label "Google Maps API" actually covers a fairly broad set of separate APIs. The best known of them are the in-browser APIs, for embedding maps in webpages, and manipulating them. You've doubtless seen them used extensively around the web. Only slightly less well known is
never go against a Sicilian when death is on the line the Static Maps API and the Geocoding Web Service.
Geocoding Web Service
The Geocoding Web Service is pretty straightforward: You supply it with an address, and it supplies you with its latitude and longitude. It also provides a great deal of additional information, such as authoritative names for the various parts of the address, and a viewport that encompasses the geocoded location. Here's an example geocoding API request:
The last part of the path specifies the format - we're using JSON because it's simpler to parse and manipulate in Python, but XML is also an option. Also note the 'sensor' parameter. This parameter is mandatory for all requests, and specifies if the app doing the geocoding got its data from a sensor of some form - in our case, the answer is 'no'.
Since we only care about the latitude and longitude, not any of the other data returned, here's our straightforward Python code for doing a geocode using URLFetch:
def get_geocode(address): sig, url = make_signed_url('maps.google.com', '/maps/api/geocode/json', [ (u'address', address), (u'sensor', u'false'), ], MAPS_CLIENT_ID, MAPS_KEY) response = simplejson.loads(urlfetch.fetch(url).content) if response['status'] == 'OVER_QUERY_LIMIT': raise OverQuotaError() elif response['status'] == 'ZERO_RESULTS': result = None elif response['status'] == 'OK': result = decode_geocode_response(response['results']) else: raise Exception(response) return result
Most of this is pretty simple: We call urlfetch.fetch, pass the result to simplejson.loads, and return the first result. But what's this 'make_signed_url' method all about?
Although the API provides for limited free usage, it's also possible to sign up for Premier Edition, which provides for increased quotas. It also improves some other features - for example, Premier users can generate larger images using the Static Maps API. If you do sign up, you're provided with a client ID and a secret key, and you're expected to sign URLs using the secret key. Fortunately, the URL signing process is pretty simple:
def make_signed_url(domain, path, query, client_id, key): query = [(x.encode('utf-8'), y.encode('utf-8')) for x, y in query] query.append(('client', client_id)) path = u'%s?%s' % (path, urllib.urlencode(query)) sig = base64.urlsafe_b64encode(hmac.new(key, path, hashlib.sha1).digest()) return sig, u'http://%s%s&signature=%s' % (domain, path, sig)
This method takes the domain, the request path, and the query, as a list of tuples. It also takes your client ID and secret key. After encoding the query in UTF-8, and appending the client ID, it constructs the complete path - everything except the domain part of the URL - and uses the hmac-sha1 algorithm to compute a digest using the provided key. The signature is then tacked on to the end of the signed URL, and returned.
Static Maps API
The other component that Mapvelopes makes use of in its mashup is the Static Maps API. The Static Maps API is pretty much custom designed for what we're doing here: Given a set of map drawing parameters, it returns a pre-rendered map in PNG or JPEG format. The map looks exactly as it would if it were generated client side using the regular maps API, including labels, markers, and so forth. It will even figure out the appropriate bounds for a map given a set of markers and output dimensions!
The Static Maps API has a lot more input parameters, understandably, though we'll only need to use a few of them. 'size' defines the dimensions of the image, in pixels. Mapvelopes figures this out based on the dimensions of the envelope and the desired DPI. 'format' specifies the image format - we use 'jpeg-baseline' for a non-progressive JPEG that can be embedded directly into the PDF. 'markers' specifies markers to include on the map, and the ever-present 'sensor' parameter specifies if we used a sensor in constructing the request. Here's the simplified code to render our envelope's map:
def create_map(size, start_pos, end_pos): start_marker =u'icon:%s|%s' % (START_MARKER, start_pos) end_marker =u'icon:%s|%s' % (END_MARKER, end_pos) query = [ (u'size', size), (u'format', u'jpg-baseline'), (u'sensor', u'false'), (u'markers', start_marker), (u'markers', end_marker), ] return make_signed_url('maps.google.com', '/maps/api/staticmap', query, MAPS_CLIENT_ID, MAPS_KEY)
The markers parameter accepts several formats; here we're using the 'icon' format, which allows us to specify a custom image to use for the marker icon. We can make use of the built in 'start' and 'end' marker icons provided by maps - for example, START_MARKER is 'http://www.google.com/mapfiles/dd-start.png'. start_pos and end_pos are strings of the form "lat,lon" - eg, "12.45,39.939". After generating the map using this method, we draw it to the PDF canvas in the manner we described in the previous post:
map_url = create_map(ENVELOPE_PIXEL_SIZES[size_name], *locations) map_data = urlfetch.fetch(map_url).content map_image = canvas.ImageReader(StringIO.StringIO(map_data)) p.drawImage(map_image, 0, 0, size, size)
There are many other potential uses of the Maps APIs. Do you have one in mind? Let us know in the comments!Previous Post Next Post