diff --git a/TwitterGeoPics/pygeocoder.py b/TwitterGeoPics/pygeocoder.py new file mode 100644 index 0000000..dd4798c --- /dev/null +++ b/TwitterGeoPics/pygeocoder.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# +# Xiao Yu - Montreal - 2010 +# Based on googlemaps by John Kleint +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +""" +Python wrapper for Google Geocoding API V3. + +* **Geocoding**: convert a postal address to latitude and longitude +* **Reverse Geocoding**: find the nearest address to coordinates + +""" + +import requests +import functools +import base64 +import hmac +import hashlib +from pygeolib import GeocoderError, GeocoderResult +#from __version__ import VERSION + +try: + import json +except ImportError: + import simplejson as json + +__all__ = ['Geocoder', 'GeocoderError', 'GeocoderResult'] + + +# this decorator lets me use methods as both static and instance methods +class omnimethod(object): + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + return functools.partial(self.func, instance) + + +class Geocoder(object): + """ + A Python wrapper for Google Geocoding V3's API + + """ + + GEOCODE_QUERY_URL = 'https://maps.google.com/maps/api/geocode/json?' + + def __init__(self, client_id=None, private_key=None): + """ + Create a new :class:`Geocoder` object using the given `client_id` and + `referrer_url`. + + :param client_id: Google Maps Premier API key + :type client_id: string + + Google Maps API Premier users can provide his key to make 100,000 requests + a day vs the standard 2,500 requests a day without a key + + """ + self.client_id = client_id + self.private_key = private_key + + @omnimethod + def get_data(self, params={}): + """ + Retrieve a JSON object from a (parameterized) URL. + + :param params: Dictionary mapping (string) query parameters to values + :type params: dict + :return: JSON object with the data fetched from that URL as a JSON-format object. + :rtype: (dict or array) + + """ + request = requests.Request('GET', + url = Geocoder.GEOCODE_QUERY_URL, + params = params, + headers = { + 'User-Agent': 'pygeocoder/' + VERSION + ' (Python)' + }) + + if self and self.client_id and self.private_key: + self.add_signature(request) + + response = requests.Session().send(request.prepare()) + if response.status_code == 403: + raise GeocoderError("Forbidden, 403", response.url) + response_json = response.json() + + if response_json['status'] != GeocoderError.G_GEO_OK: + raise GeocoderError(response_json['status'], response.url) + return response_json['results'] + + @omnimethod + def add_signature(self, request): + decoded_key = base64.urlsafe_b64decode(str(self.private_key)) + signature = hmac.new(decoded_key, request.url, hashlib.sha1) + encoded_signature = base64.urlsafe_b64encode(signature.digest()) + request.params['client'] = str(self.client_id) + request.params['signature'] = encoded_signature + + @omnimethod + def geocode(self, address, sensor='false', bounds='', region='', language=''): + """ + Given a string address, return a dictionary of information about + that location, including its latitude and longitude. + + :param address: Address of location to be geocoded. + :type address: string + :param sensor: ``'true'`` if the address is coming from, say, a GPS device. + :type sensor: string + :param bounds: The bounding box of the viewport within which to bias geocode results more prominently. + :type bounds: string + :param region: The region code, specified as a ccTLD ("top-level domain") two-character value for biasing + :type region: string + :param language: The language in which to return results. + :type language: string + :returns: `geocoder return value`_ dictionary + :rtype: dict + :raises GeocoderError: if there is something wrong with the query. + + For details on the input parameters, visit + http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests + + For details on the output, visit + http://code.google.com/apis/maps/documentation/geocoding/#GeocodingResponses + + """ + + params = { + 'address': address, + 'sensor': sensor, + 'bounds': bounds, + 'region': region, + 'language': language, + } + if self is not None: + return GeocoderResult(self.get_data(params=params)) + else: + return GeocoderResult(Geocoder.get_data(params=params)) + + @omnimethod + def reverse_geocode(self, lat, lng, sensor='false', bounds='', region='', language=''): + """ + Converts a (latitude, longitude) pair to an address. + + :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 GeocoderError: If the coordinates could not be reverse geocoded. + + Keyword arguments and return value are identical to those of :meth:`geocode()`. + + For details on the input parameters, visit + http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests + + For details on the output, visit + http://code.google.com/apis/maps/documentation/geocoding/#ReverseGeocoding + + """ + params = { + 'latlng': "%f,%f" % (lat, lng), + 'sensor': sensor, + 'bounds': bounds, + 'region': region, + 'language': language, + } + + if self is not None: + return GeocoderResult(self.get_data(params=params)) + else: + return GeocoderResult(Geocoder.get_data(params=params)) + +if __name__ == "__main__": + import sys + from optparse import OptionParser + + def main(): + """ + Geocodes a location given on the command line. + + Usage: + pygeocoder.py "1600 amphitheatre mountain view ca" [YOUR_API_KEY] + pygeocoder.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. + + """ + usage = "usage: %prog [options] address" + parser = OptionParser(usage, version=VERSION) + parser.add_option("-k", "--key", dest="key", help="Your Google Maps API key") + (options, args) = parser.parse_args() + + if len(args) != 1: + parser.print_usage() + sys.exit(1) + + query = args[0] + gcoder = Geocoder(options.key) + + try: + result = gcoder.geocode(query) + except GeocoderError as err: + sys.stderr.write('%s\n%s\nResponse:\n' % (err.url, err)) + json.dump(err.response, sys.stderr, indent=4) + sys.exit(1) + + print(result) + print(result.coordinates) + main() diff --git a/TwitterGeoPics/pygeolib.py b/TwitterGeoPics/pygeolib.py new file mode 100644 index 0000000..540c7e7 --- /dev/null +++ b/TwitterGeoPics/pygeolib.py @@ -0,0 +1,174 @@ +import sys +import collections + +class GeocoderResult(collections.Iterator): + """ + A geocoder resultset to iterate through address results. + Exemple: + + results = Geocoder.geocode('paris, us') + for result in results: + print(result.formatted_address, result.location) + + Provide shortcut to ease field retrieval, looking at 'types' in each + 'address_components'. + Example: + result.country + result.postal_code + + You can also choose a different property to display for each lookup type. + Example: + result.country__short_name + + By default, use 'long_name' property of lookup type, so: + result.country + and: + result.country__long_name + are equivalent. + """ + + attribute_mapping = { + "state": "administrative_area_level_1", + "province": "administrative_area_level_1", + "city": "locality", + "county": "administrative_area_level_2", + } + + def __init__(self, data): + """ + Creates instance of GeocoderResult from the provided JSON data array + """ + self.data = data + self.len = len(self.data) + self.current_index = 0 + self.current_data = self.data[0] + + def __len__(self): + return self.len + + def __iter__(self): + return self + + def return_next(self): + if self.current_index >= self.len: + raise StopIteration + self.current_data = self.data[self.current_index] + self.current_index += 1 + return self + + def __getitem__(self, key): + """ + Accessing GeocoderResult by index will return a GeocoderResult + with just one data entry + """ + return GeocoderResult([self.data[key]]) + + def __unicode__(self): + return self.formatted_address + + if sys.version_info[0] >= 3: # Python 3 + def __str__(self): + return self.__unicode__() + + def __next__(self): + return self.return_next() + else: # Python 2 + def __str__(self): + return self.__unicode__().encode('utf8') + + def next(self): + return self.return_next() + + @property + def count(self): + return self.len + + @property + def coordinates(self): + """ + Return a (latitude, longitude) coordinate pair of the current result + """ + location = self.current_data['geometry']['location'] + return location['lat'], location['lng'] + + @property + def latitude(self): + return self.coordinates[0] + + @property + def longitude(self): + return self.coordinates[1] + + @property + def raw(self): + """ + Returns the full result set in dictionary format + """ + return self.data + + @property + def valid_address(self): + """ + Returns true if queried address is valid street address + """ + return self.current_data['types'] == [u'street_address'] + + @property + def formatted_address(self): + return self.current_data['formatted_address'] + + def __getattr__(self, name): + lookup = name.split('__') + attribute = lookup[0] + + if (attribute in GeocoderResult.attribute_mapping): + attribute = GeocoderResult.attribute_mapping[attribute] + + try: + prop = lookup[1] + except IndexError: + prop = 'long_name' + + for elem in self.current_data['address_components']: + if attribute in elem['types']: + return elem[prop] + + +class GeocoderError(Exception): + """Base class for errors in the :mod:`pygeocoder` module. + + Methods of the :class:`Geocoder` 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_OK = "OK" + G_GEO_ZERO_RESULTS = "ZERO_RESULTS" + G_GEO_OVER_QUERY_LIMIT = "OVER_QUERY_LIMIT" + G_GEO_REQUEST_DENIED = "REQUEST_DENIED" + G_GEO_MISSING_QUERY = "INVALID_REQUEST" + + 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.url = url + self.response = response + + def __str__(self): + """Return a string representation of this :exc:`GeocoderError`.""" + return 'Error %s\nQuery: %s' % (self.status, self.url) + + def __unicode__(self): + """Return a unicode representation of this :exc:`GeocoderError`.""" + return unicode(self.__str__())