Commits

Anonymous committed 82d1aa9

Set up repo

Comments (0)

Files changed (6)

+syntax: glob
+*.pyc
+*~
+googleapikey
+html/
+data/
+gis/
+'''
+Where We Meet
+
+Tools for constructing a google map with contour lines of areas easiest for members of a group to get to.
+ 
+Written by Elliot Hallmark (permafacture@gmail.com)
+
+Free to use.  Have fun.
+'''
+
+
+
+from googlemaps import GoogleMaps,GoogleMapsError  
+import pygmaps
+#import pyshp
+
+from numpy import arange, linspace, zeros #is aranged still used?
+import csv
+
+#lat, long of boundry defining points
+ur = [30.494243, -97.430981] #lat,long of lower left corner of area to consider
+ll = [30.116622, -97.76881] #lat,long of upper right corner of area to consider
+
+center = [(ll[0]+ur[0])/2,(ll[1]+ur[1])/2]
+
+gridfilename = 'data/_gridfile.csv'
+
+try:
+    with open('./googleapikey') as f:
+        GMAPS_API_KEY = f.readline().rstrip()
+except IOError:
+    print '''
+    FATAL ERROR: You don't have a google API key set up yet!
+    Get your key from google.  Then put it in a file called 'googleapikey' in
+    the same directory as this script.\n'''
+    raise
+    
+
+#this will be needed globally
+gmaps = GoogleMaps(GMAPS_API_KEY, referrer_url='http://www.google.com/')
+
+
+def traveltime(location1,location2):
+    '''Use google maps to calculate travel time between tow locations.
+    This function retries a few times if query to google fails.'''
+
+    #retries are necessary.
+    for tries in range(5):
+        try:
+            results = gmaps.directions(location1,location2)
+        except GoogleMapsError:
+            pass #keep trying, should i phutz with location values to coax a good response?
+        else:
+            #success
+            return results ['Directions']['Duration']['seconds']
+    else:
+        raise GoogleMapsError('\nlocation1: %s \nlocation2: %s' % (location1,location2))
+
+def gen_grid_file(ll, ur, divisions=30.):
+    '''Generate a csv of points that all other data files will refer to.
+    This file is defined within this module.  Edit the source code to change it.
+    input the lower left and upper right corners of the area to be anaylzed'''
+
+    from os.path import isfile
+    if isfile(gridfilename):
+        print '''
+    There is data in the data directory already!
+    Creating a new gridfile will corrupt that data.
+    If you meant to start fresh, please delete or move the gridfile and other csv files and try again.
+
+        '''
+        raise IOError
+         
+    llx,lly = ll
+    urx,ury = ur
+    #things go from right to left, and lower to upper
+    #xspacing = float((urx-llx))/divisions
+    #yspacing = float((ury-lly))/divisions
+    xs = linspace(llx,urx,divisions)
+    ys = linspace(lly,ury,divisions)
+    with open(gridfilename, 'wb') as csvfile:
+        csvwriter = csv.writer(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_MINIMAL)
+        for y in ys:
+            row=[]
+            for x in xs:
+                row.append(str([x,y]))
+            csvwriter.writerow(row)
+    
+def time_map_from_address(source, filename='data/test.csv'):
+    '''Uses the gridfile and creates a csv file of travel times from source to all grid points.
+    The data is saved in 'filename'
+    returns # of results written and # of errors.'''
+
+    errors = 0
+    results = 0
+    with open(gridfilename, 'rb') as gridfile:
+     with open(filename,'wb') as outfile:
+        reader = csv.reader(gridfile, delimiter='\t', quotechar='"')
+        writer = csv.writer(outfile, delimiter='\t', quotechar='"')
+        for row in reader:
+            result_row = []
+            for item in row:
+                #eval usage stolen from internet: string representation of list to actual list
+                try: 
+                    result = traveltime(source,tuple(eval(item, {'__builtins__':None}, {})))
+                except GoogleMapsError:
+                    result = None
+                    errors += 1
+                results +=1
+                result_row.append(str(result))
+            writer.writerow(result_row)
+    return results,errors
+
+def coords_of_nones(filename='data/elliot.csv'):
+    '''For Debugging. Errors in google maps pathing create None values.
+    this function uses the local gridfile and returns the coordinates of failed queries in "filename".'''
+    result = []
+    with open(gridfilename, 'rb') as grid:
+     with open(filename,'rb') as data:
+        grid_reader = csv.reader(grid, delimiter='\t', quotechar='"')
+        data_reader = csv.reader(data, delimiter='\t', quotechar='"')
+        for datarow in data_reader:
+            gridrow=grid_reader.next()
+            gridrow=gridrow.__iter__()
+            for item in datarow:
+                gitem=gridrow.next()
+                if item == 'None':
+                    #print gitem
+                    result.append(tuple(eval(gitem, {'__builtins__':None}, {}))) 
+    return result
+
+def patch_data(source,filename):
+    '''If a travel time csv file still has Nones in it, this function will try and patch it.
+    If pathcing fails, there is probably some other problem (location unreachable, no connection to google, etc.'''
+    nones = coords_of_nones(filename=filename)
+    patches = []
+    i=0
+    for non in nones:
+        #TODO if traveltime fails again, this function should just put in a really high travel time.
+        #the location could be unreachable.
+        patches.append(traveltime(source,non))
+    with open(filename, 'rb') as data:
+     with open('data/patched.csv','wb') as result:
+        result_writer = csv.writer(result, delimiter='\t', quotechar='"')
+        data_reader = csv.reader(data, delimiter='\t', quotechar='"')
+        for datarow in data_reader:
+            result = []
+            for item in datarow:
+                if item != 'None':
+                    result.append(item)
+                else:
+                    #nones are encountered in the same order as patches was constructed
+                    result.append(patches[i])
+                    i+=1
+            result_writer.writerow(result)
+
+def get_contour_paths(filename, n=10):
+    '''Uses gridfile and the file at 'filename' to return contour paths, colors and values''' 
+    #TODO: currently this plot is rotated with respect to google map
+    import matplotlib.pyplot as plt
+    
+    Xs = []
+    Ys = []
+    Zs = []
+    #extract data from files
+    with open(gridfilename, 'rb') as grid:
+        #not using with..as because filename could be file or array
+        data = open(filename,'rb') 
+        grid_reader = csv.reader(grid, delimiter='\t', quotechar='"')
+        data_reader = csv.reader(data, delimiter='\t', quotechar='"')
+        #print 'hello2'
+        for datarow in data_reader:
+            #print 'hello'
+            gridrow=grid_reader.next()
+            gridrow=gridrow.__iter__()
+            row = []
+            for item in datarow:
+                gitem = gridrow.next()
+                gitem = tuple(eval(gitem, {'__builtins__':None}, {}))
+                Xs.append(gitem[0])
+                Ys.append(gitem[1])
+                row.append(item)
+            Zs.append(row)
+    #plt.figure()
+    #filter Xs and Ys to be what contour expects. the Xs and Ys of a grid
+    m = len(Zs[0])
+    Xs = Xs[:m]
+    Ys = Ys[::m]
+    C = plt.contour(Xs,Ys,Zs,n)
+    #plt.show()
+    levels = []
+    colors = []
+    for collection in C.collections:
+        colors.append(collection.get_color())
+        ps = []
+        for p in collection.get_paths():
+          ps.append(p.vertices)
+        levels.append(ps)
+    #paths, colors of paths, and values represented by paths
+    return levels,colors,C.levels
+
+def sum_files(source_path="data/",save_file='_sumfile.csv'):
+    '''create a file that sums all the travel times from all csv that don't start with '_' in path.'''
+    import os
+    path=source_path
+    #first determine size of the grid from _gridfile.csv
+    with open(gridfilename,'rb') as f:
+     r = csv.reader(f,delimiter='\t', quotechar='"')
+     c=0
+     for row in r:
+        c+=1
+    result = zeros((c,len(row)))
+
+    #now iterate through files and sum fields
+    for f in os.listdir(path):
+        ff = os.path.join(path,f)
+        if f[0] != '_' and os.path.isfile(ff):
+          with open(ff,'rb') as fff:
+            r = csv.reader(fff,delimiter='\t', quotechar='"')
+            print 'open'
+            for i,row in enumerate(r):
+                for j,item in enumerate(row):
+                    result[i][j] += float(item)
+
+    #turn result into a file.  I thought I could use the array directly but guess not
+    with open(os.path.join(path,save_file),'wb') as f:
+        w = csv.writer(f,delimiter='\t', quotechar='"')
+        for row in result:
+            w.writerow(row)
+    
+    return result
+
+def process_users(filename, output_path ="data/"):
+    '''create data files for all users on a spreadsheet'''
+    import os
+    path=output_path
+    ls = os.listdir(path)
+    with open(filename,'rb') as f:
+        reader = csv.DictReader(f,delimiter='\t')
+        for row in reader:
+            name = row['user'].lower()
+            print "This will take a while.  processing user %s..." % (name,)
+            if '%s.csv' % (name,) in ls:
+                print 'previously processed'
+            else:
+                print 'Starting time map...'
+                res,err = time_map_from_address(row['address'], filename='data/%s.csv' % name)
+                print 'finished %i quieries with %i errors' % (res,err)
+                if err:
+                    print """\nYou should run 'patch_data' on data/%s.csv'. You must respecify the address.""" %(name,) 
+
+if __name__ == '__main__':
+
+    #gen a map of travel times for source. 
+    #area analyzed is defined by global variables at top of file
+    #This is a demonstration of how to use the program.
+
+    #run this once per area you are analyzing.  You can add to this data set whenever
+    #generate grid of lats and longs for the working area
+    #gen_grid_file(ll,ur,divisions=40)
+
+    #generate traveling times* (time intensive)
+
+    #  *for one address only:
+    '''
+    source = 'bennu coffee, Austin Tx 78702'
+    results = time_map_from_address(source, filename='data/001.csv')
+    print 'time map results: %s, errors: %s' % tuple(results)    
+    '''
+    #  *for addresses defined in ./data/_address_list.csv
+    #if user was already processed, they wont be processed again when this is run
+    address_file = 'data/_address_list.csv'
+    process_users(address_file, output_path ="data/")
+    
+    #calculate total driving time of all users to each point on grid
+    #a different function could weight users differently in making this map
+    sum_files(source_path="data/",save_file='_sumfile.csv')
+
+    #calculate contour data from csv file
+    levels,colors,legend = get_contour_paths('data/_sumfile.csv',n=20)
+    
+    #set up map
+    mymap = pygmaps.maps(center[0], center[1], 10)
+
+    #display bounding box
+    x1,y1 = ll
+    x2,y2 = ur
+    path = [(x1,y1), (x1,y2),(x2,y2),(x2,y1),(x1,y1)]
+    mymap.addpath(path,color="#0000FF",fillcolor = "#000000", opacity=.2)
+
+    #plot the acceptable area for final presentation
+    acceptable_level = int(1)   #index to the contour level that is acceptable
+    color = '#5555ee'
+    print 'Highlighted area is under %s minutes.' % (legend[acceptable_level]/60.)
+    for i,path in enumerate(levels[acceptable_level]):
+            p =  map(tuple,path)   #contour includes full 
+            mymap.addpath(p,color=color,fillcolor=color,opacity=.5)
+
+    #plot the other contour levels
+    #select approriate lines to plot from 'levels' using slices
+    for i,paths in enumerate(levels[::3][:3]):
+        #colors slowly turns from blue to red, slice it differently to get faster change in color
+        color='#%0.2x%0.2x%0.2x' % tuple([q*255 for q in colors[i*4][0][:-1]])
+        for i,path in enumerate(paths):
+            p =  map(tuple,path)    
+            mymap.addpath(p,color=color,fillcolor=color,opacity=0)
+
+    print "contour values: %s" % (legend,) 
+    mymap.draw('./html/mymap.html')

data/_address_list.csv

+"user"	"address"	"commute"
+"coffee shop"	"thunderbird coffee, 78722"	
+"Person1"	"parmer and mopac, austin texas"	
+#!/usr/bin/env python
+
+# Copyright 2009 John Kleint
+#
+# This is free software, licensed under the Lesser Affero General 
+# Public License, available in the accompanying LICENSE.txt file.
+
+
+"""
+An easy-to-use Python wrapper for the Google Maps and Local Search APIs.
+
+* **Geocoding**: convert a postal address to latitude and longitude
+* **Reverse Geocoding**: find the nearest address to (lat, lng)
+* **Local Search**: find places matching a query near a given location
+* **Directions**: turn-by-turn directions, distance, time, etc. from A to B
+
+"""
+
+
+import urllib
+import urllib2
+try:
+    import json
+except ImportError:
+    import simplejson as json       # pylint: disable-msg=F0401
+
+
+VERSION = '1.0.2'
+__all__ = ['GoogleMaps', 'GoogleMapsError']
+
+
+def fetch_json(query_url, params={}, headers={}):       # pylint: disable-msg=W0102
+    """Retrieve a JSON object from a (parameterized) URL.
+    
+    :param query_url: The base URL to query
+    :type query_url: string
+    :param params: Dictionary mapping (string) query parameters to values
+    :type params: dict
+    :param headers: Dictionary giving (string) HTTP headers and values
+    :type headers: dict 
+    :return: A `(url, json_obj)` tuple, where `url` is the final,
+    parameterized, encoded URL fetched, and `json_obj` is the data 
+    fetched from that URL as a JSON-format object. 
+    :rtype: (string, dict or array)
+    
+    """
+    encoded_params = urllib.urlencode(params)    
+    url = query_url + encoded_params
+    request = urllib2.Request(url, headers=headers)
+    response = urllib2.urlopen(request)
+    return (url, json.load(response))
+        
+
+class GoogleMapsError(Exception):
+    """Base class for errors in the :mod:`googlemaps` module.
+    
+    Methods of the :class:`GoogleMaps` raise this when something goes wrong.
+     
+    """
+    #: See http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes
+    #: for information on the meaning of these status codes.
+    G_GEO_SUCCESS               = 200
+    G_GEO_SERVER_ERROR          = 500
+    G_GEO_MISSING_QUERY         = 601
+    G_GEO_UNKNOWN_ADDRESS       = 602
+    G_GEO_UNAVAILABLE_ADDRESS   = 603
+    G_GEO_BAD_KEY               = 610
+    G_GEO_TOO_MANY_QUERIES      = 620   
+
+    _STATUS_MESSAGES = {
+        G_GEO_SUCCESS               : 'G_GEO_SUCCESS',
+        G_GEO_SERVER_ERROR          : 'G_GEO_SERVER_ERROR',
+        G_GEO_MISSING_QUERY         : 'G_GEO_MISSING_QUERY',
+        G_GEO_UNKNOWN_ADDRESS       : 'G_GEO_UNKNOWN_ADDRESS',
+        G_GEO_UNAVAILABLE_ADDRESS   : 'G_GEO_UNAVAILABLE_ADDRESS',
+        G_GEO_BAD_KEY               : 'G_GEO_BAD_KEY',
+        G_GEO_TOO_MANY_QUERIES      : 'G_GEO_TOO_MANY_QUERIES',
+    }
+    
+    def __init__(self, status, url=None, response=None):
+        """Create an exception with a status and optional full response.
+        
+        :param status: Either a ``G_GEO_`` code or a string explaining the 
+         exception.
+        :type status: int or string
+        :param url: The query URL that resulted in the error, if any.
+        :type url: string
+        :param response: The actual response returned from Google, if any.
+        :type response: dict 
+        
+        """
+        Exception.__init__(self, status)        # Exception is an old-school class
+        self.status = status
+        self.response = response
+        self.url = url
+        
+    def __str__(self):
+        """Return a string representation of this :exc:`GoogleMapsError`."""
+        if self.status in self._STATUS_MESSAGES:
+            if self.response is not None and 'responseDetails' in self.response:
+                retval = 'Error %d: %s' % (self.status, self.response['responseDetails'])
+            else:
+                retval = 'Error %d: %s' % (self.status, self._STATUS_MESSAGES[self.status])
+        else:
+            retval = str(self.status)
+        return retval
+    
+    def __unicode__(self):
+        """Return a unicode representation of this :exc:`GoogleMapsError`."""
+        return unicode(self.__str__())
+
+
+STATUS_OK = GoogleMapsError.G_GEO_SUCCESS
+
+
+class GoogleMaps(object):
+    """
+    An easy-to-use Python wrapper for the Google Maps and Local Search APIs.
+
+    **Geocoding**: convert a postal address to latitude and longitude
+    
+    >>> from googlemaps import GoogleMaps
+    >>> gmaps = GoogleMaps(api_key)
+    >>> address = 'Constitution Ave NW & 10th St NW, Washington, DC'
+    >>> lat, lng = gmaps.address_to_latlng(address)
+    >>> print lat, lng
+    38.8921021 -77.0260358
+    
+    **Reverse Geocoding**: find the nearest address to (lat, lng)
+        
+    >>> destination = gmaps.latlng_to_address(38.887563, -77.019929)
+    >>> print destination
+    Independence and 6th SW, Washington, DC 20024, USA
+    
+    **Local Search**: find places matching a query near a given location
+    
+    >>> local = gmaps.local_search('cafe near ' + destination)
+    >>> print local['responseData']['results'][0]['titleNoFormatting']
+    Vie De France Bakery & Cafe
+    
+    **Directions**: turn-by-turn directions, distance, time, etc. from 
+    point A to point B
+    
+    >>> directions = gmaps.directions(address, destination)
+    >>> print directions['Directions']['Distance']['meters']
+    1029
+    >>> print directions['Directions']['Duration']['seconds']
+    106
+    >>> for step in directions['Directions']['Routes'][0]['Steps']:
+    ...     print step['descriptionHtml']
+    Head <b>east</b> on <b>Constitution Ave NW</b> toward <b>9th St NW</b>
+    Take the 2nd <b>right</b> onto <b>7th St NW</b>
+    Turn <b>left</b> at <b>Independence Ave SW</b>
+    
+    This software is in no way associated with or endorsed by Google Inc.
+    Use of the Google Maps API is governed by its Terms of Service:
+    http://code.google.com/apis/maps/terms.html.  Note in particular that
+    you will need your own Google Maps API key to use this service,
+    and that there are rate limits to the number of requests you can
+    make.
+    
+    """
+
+    _GEOCODE_QUERY_URL = 'http://maps.google.com/maps/geo?'
+    _DIRECTIONS_QUERY_URL = 'http://maps.google.com/maps/nav?'
+    _LOCAL_QUERY_URL = 'http://ajax.googleapis.com/ajax/services/search/local?'
+    _LOCAL_RESULTS_PER_PAGE = 8
+    MAX_LOCAL_RESULTS = 32
+
+    def __init__(self, api_key='', referrer_url=''):
+        """
+        Create a new :class:`GoogleMaps` object using the given `api_key` and 
+        `referrer_url`.
+        
+        :param api_key: Google Maps API key 
+        :type api_key: string
+        :param referrer_url: URL of the website using or displaying information
+         from this module.
+        :type referrer_url: string
+        
+        Google requires API users to register for an API key before using the
+        geocoding service; this can be done at
+        http://code.google.com/apis/maps/signup.html.
+        If you are not geocoding, you do not need an API key.
+        
+        Use of Google Local Search requires a referrer URL, generally the website
+        where the retrieved information will be used.  If you are not using
+        Local Search, you do not need a referrer URL.
+        
+        """
+        self.api_key = api_key
+        self.referrer_url = referrer_url
+        
+    def geocode(self, query, sensor='false', oe='utf8', ll='', spn='', gl=''):       # pylint: disable-msg=C0103,R0913
+        """
+        Given a string address `query`, return a dictionary of information about
+        that location, including its latitude and longitude.
+        
+        Interesting bits:
+        
+        >>> gmaps = GoogleMaps(api_key)
+        >>> address = '350 Fifth Avenue New York, NY'
+        >>> result = gmaps.geocode(address)
+        >>> placemark = result['Placemark'][0]
+        >>> lng, lat = placemark['Point']['coordinates'][0:2]    # Note these are backwards from usual
+        >>> print lat, lng
+        40.6721118 -73.9838823
+        >>> details = placemark['AddressDetails']['Country']['AdministrativeArea']
+        >>> street = details['Locality']['Thoroughfare']['ThoroughfareName']
+        >>> city = details['Locality']['LocalityName']
+        >>> state = details['AdministrativeAreaName']
+        >>> zipcode = details['Locality']['PostalCode']['PostalCodeNumber']
+        >>> print ', '.join((street, city, state, zipcode))
+        350 5th Ave, Brooklyn, NY, 11215
+
+        More documentation on the format of the return value can be found at 
+        Google's `geocoder return value`_ reference.  (Note: Some places have 
+        a `'SubAdministrativeArea'` and some don't; sometimes a `'Locality'` 
+        will have a `'DependentLocality'` and some don't.)
+        
+        .. _`geocoder return value`: http://code.google.com/apis/maps/documentation/geocoding/index.html#JSON
+
+        :param query: Address of location to be geocoded.
+        :type query: string
+        :param sensor: ``'true'`` if the address is coming from, say, a GPS device.
+        :type sensor: string
+        :param oe: Output Encoding; best left at ``'utf8'``.
+        :type oe: string
+        :param ll: `lat,lng` of the viewport center as comma-separated string;
+         must be used with `spn` for Viewport Biasing
+        :type ll: string  
+        :param spn: The "span" of the viewport; must be used with `ll`.
+        :type spn: string
+        :param gl: Two-character ccTLD_ for country code biasing. 
+        :type gl: string
+        :returns: `geocoder return value`_ dictionary
+        :rtype: dict 
+        :raises GoogleMapsError: if there is something wrong with the query.
+        
+        More information on the types and meaning of the parameters can be found
+        at the `Google HTTP Geocoder`__ site.
+        
+        __ http://code.google.com/apis/maps/documentation/geocoding/index.html
+        .. _ccTLD: http://en.wikipedia.org/wiki/Country_code_top-level_domain
+        
+        """
+        if (ll is None and spn is not None) or (ll is not None and spn is None):
+            raise GoogleMapsError('Both ll and spn must be provided.')
+        params = {
+            'q':        query,
+            'key':      self.api_key,
+            'sensor':   sensor,
+            'output':   'json',
+            'oe':       oe,
+            'll':       ll,
+            'spn':      spn,
+            'gl':       gl,
+        }
+        url, response = fetch_json(self._GEOCODE_QUERY_URL, params=params)
+        status_code = response['Status']['code']
+        if status_code != STATUS_OK:
+            raise GoogleMapsError(status_code, url, response)
+        return response
+
+    def reverse_geocode(self, lat, lng, sensor='false', oe='utf8', ll='', spn='', gl=''):        # pylint: disable-msg=C0103,R0913
+        """
+        Converts a (latitude, longitude) pair to an address.
+        
+        Interesting bits:
+        
+        >>> gmaps = GoogleMaps(api_key)
+        >>> reverse = gmaps.reverse_geocode(38.887563, -77.019929)
+        >>> address = reverse['Placemark'][0]['address']
+        >>> print address
+        Independence and 6th SW, Washington, DC 20024, USA
+        >>> accuracy = reverse['Placemark'][0]['AddressDetails']['Accuracy']
+        >>> print accuracy
+        9
+        
+        :param lat: latitude
+        :type lat: float
+        :param lng: longitude
+        :type lng: float
+        :return: `Reverse geocoder return value`_ dictionary giving closest 
+            address(es) to `(lat, lng)`
+        :rtype: dict
+        :raises GoogleMapsError: If the coordinates could not be reverse geocoded.
+        
+        Keyword arguments and return value are identical to those of :meth:`geocode()`.
+        
+        .. _`Reverse geocoder return value`: 
+            http://code.google.com/apis/maps/documentation/geocoding/index.html#ReverseGeocoding
+        
+        """
+        return self.geocode("%f,%f" % (lat, lng), sensor=sensor, oe=oe, ll=ll, spn=spn, gl=gl)
+    
+    def address_to_latlng(self, address):
+        """
+        Given a string `address`, return a `(latitude, longitude)` pair.
+        
+        This is a simplified wrapper for :meth:`geocode()`.
+        
+        :param address: The postal address to geocode.
+        :type address: string
+        :return: `(latitude, longitude)` of `address`.
+        :rtype: (float, float)
+        :raises GoogleMapsError: If the address could not be geocoded.
+        
+        """
+        return tuple(self.geocode(address)['Placemark'][0]['Point']['coordinates'][1::-1])
+    
+    def latlng_to_address(self, lat, lng):
+        """
+        Given a latitude `lat` and longitude `lng`, return the closest address.
+        
+        This is a simplified wrapper for :meth:`reverse_geocode()`.
+        
+        :param lat: latitude
+        :type lat: float
+        :param lng: longitude
+        :type lng: float
+        :return: Closest postal address to `(lat, lng)`, if any.
+        :rtype: string
+        :raises GoogleMapsError: if the coordinates could not be converted
+         to an address. 
+        
+        """ 
+        return self.reverse_geocode(lat, lng)['Placemark'][0]['address']
+
+    def local_search(self, query, numresults=_LOCAL_RESULTS_PER_PAGE, **kwargs):
+        """
+        Searches Google Local for the string `query` and returns a 
+        dictionary of the results.
+    
+        >>> gmaps = GoogleMaps(api_key)
+        >>> local = gmaps.local_search('sushi san francisco, ca')
+        >>> result = local['responseData']['results'][0]
+        >>> print result['titleNoFormatting']
+        Sushi Groove
+        >>> print result['streetAddress']
+        1916 Hyde St
+        >>> print result['phoneNumbers'][0]['number']
+        (415) 440-1905
+
+        For more information on the available data, see Google's documentation on 
+        `AJAX result structure`_ and `local result properties`_.
+        
+        The return value of this method is slightly different than that
+        documented by Google; it attempts to stuff as many results as
+        possible, from several queries (up to `numresults`), into the 
+        ``['responseData']['results']`` array.  As a result, fields of
+        the results referencing this array (such as ``'cursor'``, 
+        ``'currentPageIndex'``, ``'moreResultsUrl'``) may not make
+        complete sense.
+        
+        This method may return fewer results than you ask for; Google Local 
+        returns a maximum of :data:`GoogleMaps.MAX_LOCAL_RESULTS` results.
+        
+        :param query: String containing a search and a location, such as 
+         ``'Sushi San Francisco, CA'``.
+        :type query: string
+        :param numresults: Number of results to return, up to a maximum of :data:`MAX_LOCAL_RESULTS`.
+        :type numresults: int
+        :param kwargs: You can pass additional `AJAX search arguments`_ and they 
+         will be tacked on to the query.
+        :return: A Google `AJAX result structure`_.
+        :rtype: dict
+        :raises GoogleMapsError: If the query was malformed.
+         
+        .. _AJAX result structure: http://code.google.com/apis/ajaxsearch/documentation/#fonje
+        .. _local result properties: http://code.google.com/apis/ajaxsearch/documentation/reference.html#_class_GlocalResult 
+        .. _AJAX search arguments: http://code.google.com/apis/ajaxsearch/documentation/reference.html#_intro_fonje
+        
+        """
+        params = {
+            'q':        query,
+            'v':        '1.0',
+            'rsz':      'large',            # Return 8 results per page instead of 4
+            #'key':      self.api_key,      # Google Local seems not to like empty keys
+        }
+        params.update(kwargs)
+            
+        start = 0
+        results = None
+        while start < numresults and start < self.MAX_LOCAL_RESULTS:
+            params['start'] = start
+            url, response = fetch_json(self._LOCAL_QUERY_URL, params=params, headers={'Referer': self.referrer_url})
+            status_code = response['responseStatus']
+            if status_code != STATUS_OK:
+                raise GoogleMapsError(status_code, url=url, response=response)
+            if results is None:
+                results = response
+            else: 
+                results['responseData']['results'].extend(response['responseData']['results'])
+            
+            # If we didn't get a full page of results, Google has run out; don't try again
+            if len(response['responseData']['results']) < self._LOCAL_RESULTS_PER_PAGE:
+                break
+            start += len(response['responseData']['results'])
+            
+        if results is not None:
+            results['responseData']['results'] = results['responseData']['results'][:numresults]
+        return results
+
+    def directions(self, origin, destination, **kwargs):
+        """
+        Get driving directions from `origin` to `destination`.
+
+        Interesting bits:
+
+        >>> gmaps = GoogleMaps(api_key)
+        >>> start = 'Constitution Ave NW & 10th St NW, Washington, DC'
+        >>> end   = 'Independence and 6th SW, Washington, DC 20024, USA'
+        >>> dirs  = gmaps.directions(start, end) 
+        >>> time  = dirs['Directions']['Duration']['seconds']
+        >>> dist  = dirs['Directions']['Distance']['meters']
+        >>> route = dirs['Directions']['Routes'][0]
+        >>> for step in route['Steps']:
+        ...    print step['Point']['coordinates'][1], step['Point']['coordinates'][0] 
+        ...    print step['descriptionHtml']
+        38.8921 -77.02604
+        Head <b>east</b> on <b>Constitution Ave NW</b> toward <b>9th St NW</b>
+        38.89208 -77.02191
+        Take the 2nd <b>right</b> onto <b>7th St NW</b>
+        38.88757 -77.02191
+        Turn <b>left</b> at <b>Independence Ave SW</b>
+
+        :param origin: Starting address
+        :type origin: string
+        :param destination: Ending address
+        :type destination: string
+        :param kwargs: You can pass additional URL parameters as keyword arguments, 
+         but this functionality is not documented.
+        :return: Dictionary containing driving directions.
+        :rtype: dict
+        :raises GoogleMapsError: If Google Maps was unable to find directions.
+        
+        """
+        params = {
+            'q':        'from:%s to:%s' % (origin, destination),
+            'output':   'json',
+            'oe':       'utf8',
+            'key':      self.api_key,
+        }
+        params.update(kwargs)
+
+        url, response = fetch_json(self._DIRECTIONS_QUERY_URL, params=params)
+        status_code = response['Status']['code']
+        if status_code != STATUS_OK:
+            raise GoogleMapsError(status_code, url=url, response=response)
+        return response
+    
+
+if __name__ == "__main__":
+    import sys
+    
+    def main(argv):
+        """
+        Geocodes a location given on the command line.
+        
+        Usage:
+            googlemaps.py "1600 amphitheatre mountain view ca" [YOUR_API_KEY]
+            googlemaps.py 37.4219720,-122.0841430 [YOUR_API_KEY]
+            
+        When providing a latitude and longitude on the command line, ensure
+        they are separated by a comma and no space.
+        
+        """
+
+        if len(argv) < 2 or len(argv) > 4:
+            print main.__doc__
+            sys.exit(1)
+            
+        query = argv[1]
+        if len(argv) == 3:
+            api_key = argv[2]
+        else:
+            api_key = raw_input("Google Maps API key: ")
+            
+        gmap = GoogleMaps(api_key)
+        try:
+            result = gmap.geocode(query)
+        except GoogleMapsError, err:
+            sys.stderr.write('%s\n%s\nResponse:\n' % (err.url, err))
+            json.dump(err.response, sys.stderr, indent=4)
+            sys.exit(1)
+        json.dump(result, sys.stdout, indent=4)
+        sys.stdout.write('\n')
+        
+    main(sys.argv)
+    
+This is a placeholder.  This should be overwritten by the mainscript.
+import math
+###########################################################
+## Google map python wrapper V0.15
+## 
+## original found at http://code.google.com/p/pygmaps/
+## This version is patched by Elliot Hallmark
+##
+############################################################
+
+class maps:
+
+    def __init__(self, centerLat, centerLng, zoom ):
+        self.center = (float(centerLat),float(centerLng))
+        self.zoom = int(zoom)
+        self.grids = None
+        self.paths = []
+        self.points = []
+        self.radpoints = []
+        self.gridsetting = None
+        self.coloricon = 'http://chart.apis.google.com/chart?cht=mm&chs=12x16&chco=FFFFFF,XXXXXX,000000&ext=.png'
+
+    def setgrids(self,slat,elat,latin,slng,elng,lngin):
+        self.gridsetting = [slat,elat,latin,slng,elng,lngin]
+
+    def addpoint(self, lat, lng, color = '#FF0000', title = None):
+        self.points.append((lat,lng,color[1:],title))
+
+    #def addpointcoord(self, coord):
+    #    self.points.append((coord[0],coord[1]))
+
+    def addradpoint(self, lat,lng,rad,color = '#0000FF'):
+	radpoint = {'pt':(lat,lng), 'rad':rad, 'color':color}
+        self.radpoints.append(radpoint)
+
+    def addpath(self,path, color = '#FF0000', fillcolor='#FF0000', opacity=False):
+	path = {'path':path, 'strokeColor':color, 'fillColor':fillcolor, 'opacity':opacity}
+        self.paths.append(path)
+    
+    #create the html file which inlcude one google map and all points and paths
+    def draw(self, htmlfile):
+        f = open(htmlfile,'w')
+        f.write('<html>\n')
+        f.write('<head>\n')
+        f.write('<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />\n')
+        f.write('<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>\n')
+        f.write('<title>Google Maps - pygmaps </title>\n')
+        f.write('<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>\n')
+        f.write('<script type="text/javascript">\n')
+        f.write('\tfunction initialize() {\n')
+        self.drawmap(f)
+        self.drawgrids(f)
+        self.drawpoints(f)
+        self.drawradpoints(f)
+        self.drawpaths(f,self.paths)
+        f.write('\t}\n')
+        f.write('</script>\n')
+        f.write('</head>\n')
+        f.write('<body style="margin:0px; padding:0px;" onload="initialize()">\n')
+        f.write('\t<div id="map_canvas" style="width: 100%; height: 100%;"></div>\n')
+        f.write('</body>\n')
+        f.write('</html>\n')        
+        f.close()
+
+    def drawgrids(self, f):
+        if self.gridsetting == None:
+            return
+        slat = self.gridsetting[0]
+        elat = self.gridsetting[1]
+        latin = self.gridsetting[2]
+        slng = self.gridsetting[3]
+        elng = self.gridsetting[4]
+        lngin = self.gridsetting[5]
+        self.grids = []
+
+        r = [slat+float(x)*latin for x in range(0, int((elat-slat)/latin))]
+        for lat in r:
+            self.grids.append([(lat+latin/2.0,slng+lngin/2.0),(lat+latin/2.0,elng+lngin/2.0)])
+
+        r = [slng+float(x)*lngin for x in range(0, int((elng-slng)/lngin))]
+        for lng in r:
+            self.grids.append([(slat+latin/2.0,lng+lngin/2.0),(elat+latin/2.0,lng+lngin/2.0)])
+        
+        for line in self.grids:
+            self.drawPolyline(f,line,strokeColor = "#000000")
+    def drawpoints(self,f):
+        for point in  self.points:
+            self.drawpoint(f,point[0],point[1],point[2],point[3])
+
+    def drawradpoints(self, f):
+        for rpoint in self.radpoints:
+            path = self.getcycle(rpoint['pt'],rpoint['rad'])
+            self.drawPolygon(f,path,strokeColor = rpoint['color'])
+
+    def getcycle(self,center,radius):
+        cycle = []
+        lat, lng = center
+        rad = radius #unit: meter
+        d = (rad/1000.0)/6378.8;
+        lat1 = (math.pi/180.0)* lat
+        lng1 = (math.pi/180.0)* lng
+
+        r = [x*30 for x in range(12)]
+        for a in r:
+            tc = (math.pi/180.0)*a;
+            y = math.asin(math.sin(lat1)*math.cos(d)+math.cos(lat1)*math.sin(d)*math.cos(tc))
+            dlng = math.atan2(math.sin(tc)*math.sin(d)*math.cos(lat1),math.cos(d)-math.sin(lat1)*math.sin(y))
+            x = ((lng1-dlng+math.pi) % (2.0*math.pi)) - math.pi 
+            cycle.append( ( float(y*(180.0/math.pi)),float(x*(180.0/math.pi)) ) )
+        return cycle
+
+    def drawpaths(self, f, paths):
+        for path in paths:
+            if not path['opacity']:
+		#fill is false
+            	self.drawPolyline(f,path['path'], strokeColor = path['strokeColor'])
+	    else:
+		self.drawPolygon(f,path['path'],strokeColor=path['strokeColor'], fillColor=path['fillColor'], fillOpacity=path['opacity'])
+    #############################################
+    # # # # # # Low level Map Drawing # # # # # # 
+    #############################################
+    def drawmap(self, f):
+        f.write('\t\tvar centerlatlng = new google.maps.LatLng(%f, %f);\n' % (self.center[0],self.center[1]))
+        f.write('\t\tvar myOptions = {\n')
+        f.write('\t\t\tzoom: %d,\n' % (self.zoom))
+        f.write('\t\t\tcenter: centerlatlng,\n')
+        f.write('\t\t\tmapTypeId: google.maps.MapTypeId.ROADMAP\n')
+        f.write('\t\t};\n')
+        f.write('\t\tvar map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);\n')
+        f.write('\n')
+
+
+
+    def drawpoint(self,f,lat,lon,color,title):
+        f.write('\t\tvar latlng = new google.maps.LatLng(%f, %f);\n'%(lat,lon))
+        f.write('\t\tvar img = new google.maps.MarkerImage(\'%s\');\n' % (self.coloricon.replace('XXXXXX',color)))
+        f.write('\t\tvar marker = new google.maps.Marker({\n')
+        if title !=None:
+            f.write('\t\ttitle: "'+str(title)+'",\n')
+        f.write('\t\ticon: img,\n')
+        f.write('\t\tposition: latlng\n')
+        f.write('\t\t});\n')
+        f.write('\t\tmarker.setMap(map);\n')
+        f.write('\n')
+        
+    def drawPolyline(self,f,path,\
+            clickable = False, \
+            geodesic = True,\
+            strokeColor = "#FF0000",\
+            strokeOpacity = 1.0,\
+            strokeWeight = 2
+            ):
+        f.write('var PolylineCoordinates = [\n')
+        for coordinate in path:
+            f.write('new google.maps.LatLng(%f, %f),\n' % (coordinate[0],coordinate[1]))
+        f.write('];\n')
+        f.write('\n')
+
+        f.write('var Path = new google.maps.Polyline({\n')
+        f.write('clickable: %s,\n' % (str(clickable).lower()))
+        f.write('geodesic: %s,\n' % (str(geodesic).lower()))
+        f.write('path: PolylineCoordinates,\n')
+        f.write('strokeColor: "%s",\n' %(strokeColor))
+        f.write('strokeOpacity: %f,\n' % (strokeOpacity))
+        f.write('strokeWeight: %d\n' % (strokeWeight))
+        f.write('});\n')
+        f.write('\n')
+        f.write('Path.setMap(map);\n')
+        f.write('\n\n')
+
+    def drawPolygon(self,f,path,\
+            clickable = False, \
+            geodesic = True,\
+            fillColor = "#000000",\
+            fillOpacity = 0.0,\
+            strokeColor = "#FF0000",\
+            strokeOpacity = 1.0,\
+            strokeWeight = 1
+            ):
+        f.write('var coords = [\n')
+        for coordinate in path:
+            f.write('new google.maps.LatLng(%f, %f),\n' % (coordinate[0],coordinate[1]))
+        f.write('];\n')
+        f.write('\n')
+
+        f.write('var polygon = new google.maps.Polygon({\n')
+        f.write('clickable: %s,\n' % (str(clickable).lower()))
+        f.write('geodesic: %s,\n' % (str(geodesic).lower()))
+        f.write('fillColor: "%s",\n' %(fillColor))
+        f.write('fillOpacity: %f,\n' % (fillOpacity))
+        f.write('paths: coords,\n')
+        f.write('strokeColor: "%s",\n' %(strokeColor))
+        f.write('strokeOpacity: %f,\n' % (strokeOpacity))
+        f.write('strokeWeight: %d\n' % (strokeWeight))
+        f.write('});\n')
+        f.write('\n')
+        f.write('polygon.setMap(map);\n')
+        f.write('\n\n')
+
+if __name__ == "__main__":
+
+    ########## CONSTRUCTOR: pygmaps(latitude, longitude, zoom) ##############################
+    # DESC:        initialize a map  with latitude and longitude of center point  
+    #        and map zoom level "15"
+    # PARAMETER1:    latitude (float) latittude of map center point
+    # PARAMETER2:    longitude (float) latittude of map center point
+    # PARAMETER3:    zoom (int)  map zoom level 0~20
+    # RETURN:    the instant of pygmaps
+    #========================================================================================
+    mymap = pygmaps(37.428, -122.145, 16)
+
+
+    ########## FUNCTION: setgrids(start-Lat, end-Lat, Lat-interval, start-Lng, end-Lng, Lng-interval) ######
+    # DESC:        set grids on map  
+    # PARAMETER1:    start-Lat (float), start (minimum) latittude of the grids
+    # PARAMETER2:    end-Lat (float), end (maximum) latittude of the grids
+    # PARAMETER3:    Lat-interval (float)  grid size in latitude 
+    # PARAMETER4:    start-Lng (float), start (minimum) longitude of the grids
+    # PARAMETER5:    end-Lng (float), end (maximum) longitude of the grids
+    # PARAMETER6:    Lng-interval (float)  grid size in longitude 
+    # RETURN:    no returns
+    #========================================================================================
+    mymap.setgrids(37.42, 37.43, 0.001, -122.15, -122.14, 0.001)
+
+
+    ########## FUNCTION:  addpoint(latitude, longitude, [color])#############################
+    # DESC:        add a point into a map and dispaly it, color is optional default is red
+    # PARAMETER1:    latitude (float) latitude of the point
+    # PARAMETER2:    longitude (float) longitude of the point
+    # PARAMETER3:    color (string) color of the point showed in map, using HTML color code
+    #        HTML COLOR CODE:  http://www.computerhope.com/htmcolor.htm
+    #        e.g. red "#FF0000", Blue "#0000FF", Green "#00FF00"
+    # RETURN:    no return
+    #========================================================================================
+    mymap.addpoint(37.427, -122.145, "#0000FF")
+
+
+    ########## FUNCTION:  addradpoint(latitude, longitude, radius, [color])##################
+    # DESC:     add a point with a radius (Meter) - Draw cycle
+    # PARAMETER1:    latitude (float) latitude of the point
+    # PARAMETER2:    longitude (float) longitude of the point
+    # PARAMETER3:    radius (float), radius  in meter 
+    # PARAMETER4:    color (string) color of the point showed in map, using HTML color code
+    #        HTML COLOR CODE:  http://www.computerhope.com/htmcolor.htm
+    #        e.g. red "#FF0000", Blue "#0000FF", Green "#00FF00"
+    # RETURN:    no return 
+    #========================================================================================
+    mymap.addradpoint(37.429, -122.145, 95, "#FF0000","hello")
+
+
+    ########## FUNCTION:  addpath(path,[color])##############################################
+    # DESC:        add a path into map, the data struceture of Path is a list of points
+    # PARAMETER1:    path (list of coordinates) e.g. [(lat1,lng1),(lat2,lng2),...]
+    # PARAMETER2:    color (string) color of the point showed in map, using HTML color code
+    #        HTML COLOR CODE:  http://www.computerhope.com/htmcolor.htm
+    #        e.g. red "#FF0000", Blue "#0000FF", Green "#00FF00"
+    # RETURN:    no return
+    #========================================================================================
+    path = [(37.429, -122.145),(37.428, -122.145),(37.427, -122.145),(37.427, -122.146),(37.427, -122.146)]
+    mymap.addpath(path,"#00FF00")
+
+    ########## FUNCTION:  addpath(file)######################################################
+    # DESC:        create the html map file (.html)
+    # PARAMETER1:    file (string) the map path and file
+    # RETURN:    no return, generate html file in specified directory
+    #========================================================================================
+    mymap.draw('./mymap.html')
+    
+