Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / geopy / geocoders / googlev3.py @ 545

History | View | Annotate | Download (11.9 KB)

1
"""
2
:class:`.GoogleV3` is the Google Maps V3 geocoder.
3
"""
4

    
5
import base64
6
import hashlib
7
import hmac
8
from geopy.compat import urlencode
9
from geopy.geocoders.base import Geocoder, DEFAULT_TIMEOUT, DEFAULT_SCHEME
10
from geopy.exc import (
11
    GeocoderQueryError,
12
    GeocoderQuotaExceeded,
13
    ConfigurationError,
14
    GeocoderParseError,
15
    GeocoderQueryError,
16
)
17
from geopy.location import Location
18
from geopy.util import logger
19

    
20
try:
21
    from pytz import timezone, UnknownTimeZoneError
22
    from calendar import timegm
23
    from datetime import datetime
24
    from numbers import Number
25
    pytz_available = True
26
except ImportError:
27
    pytz_available = False
28

    
29

    
30
__all__ = ("GoogleV3", )
31

    
32

    
33
class GoogleV3(Geocoder):  # pylint: disable=R0902
34
    """
35
    Geocoder using the Google Maps v3 API. Documentation at:
36
        https://developers.google.com/maps/documentation/geocoding/
37
    """
38

    
39
    def __init__(
40
            self,
41
            api_key=None,
42
            domain='maps.googleapis.com',
43
            scheme=DEFAULT_SCHEME,
44
            client_id=None,
45
            secret_key=None,
46
            timeout=DEFAULT_TIMEOUT,
47
            proxies=None
48
        ):  # pylint: disable=R0913
49
        """
50
        Initialize a customized Google geocoder.
51

52
        API authentication is only required for Google Maps Premier customers.
53

54
        :param string api_key: The API key required by Google to perform
55
            geocoding requests. API keys are managed through the Google APIs
56
            console (https://code.google.com/apis/console).
57

58
            .. versionadded:: 0.98.2
59

60
        :param string domain: Should be the localized Google Maps domain to
61
            connect to. The default is 'maps.google.com', but if you're
62
            geocoding address in the UK (for example), you may want to set it
63
            to 'maps.google.co.uk' to properly bias results.
64

65
        :param string scheme: Use 'https' or 'http' as the API URL's scheme.
66
            Default is https. Note that SSL connections' certificates are not
67
            verified.
68

69
            .. versionadded:: 0.97
70

71
        :param string client_id: If using premier, the account client id.
72

73
        :param string secret_key: If using premier, the account secret key.
74

75
        :param dict proxies: If specified, routes this geocoder's requests
76
            through the specified proxy. E.g., {"https": "192.0.2.0"}. For
77
            more information, see documentation on
78
            :class:`urllib2.ProxyHandler`.
79

80
            .. versionadded:: 0.96
81
        """
82
        super(GoogleV3, self).__init__(
83
            scheme=scheme, timeout=timeout, proxies=proxies
84
        )
85
        if client_id and not secret_key:
86
            raise ConfigurationError('Must provide secret_key with client_id.')
87
        if secret_key and not client_id:
88
            raise ConfigurationError('Must provide client_id with secret_key.')
89

    
90
        self.api_key = api_key
91
        self.domain = domain.strip('/')
92
        self.scheme = scheme
93
        self.doc = {}
94

    
95
        if client_id and secret_key:
96
            self.premier = True
97
            self.client_id = client_id
98
            self.secret_key = secret_key
99
        else:
100
            self.premier = False
101
            self.client_id = None
102
            self.secret_key = None
103

    
104
        self.api = '%s://%s/maps/api/geocode/json' % (self.scheme, self.domain)
105
        self.tz_api = '%s://%s/maps/api/timezone/json' % (
106
            self.scheme,
107
            self.domain
108
        )
109

    
110
    def _get_signed_url(self, params):
111
        """
112
        Returns a Premier account signed url. Docs on signature:
113
            https://developers.google.com/maps/documentation/business/webservices/auth#digital_signatures
114
        """
115
        params['client'] = self.client_id
116
        path = "?".join(('/maps/api/geocode/json', urlencode(params)))
117
        signature = hmac.new(
118
            base64.urlsafe_b64decode(self.secret_key),
119
            path.encode('utf-8'),
120
            hashlib.sha1
121
        )
122
        signature = base64.urlsafe_b64encode(
123
            signature.digest()
124
        ).decode('utf-8')
125
        return '%s://%s%s&signature=%s' % (
126
            self.scheme, self.domain, path, signature
127
        )
128

    
129
    @staticmethod
130
    def _format_components_param(components):
131
        """
132
        Format the components dict to something Google understands.
133
        """
134
        return "|".join(
135
            (":".join(item)
136
             for item in components.items()
137
            )
138
        )
139

    
140
    def geocode(
141
            self,
142
            query,
143
            exactly_one=True,
144
            timeout=None,
145
            bounds=None,
146
            region=None,
147
            components=None,
148
            language=None,
149
            sensor=False,
150
        ):  # pylint: disable=W0221,R0913
151
        """
152
        Geocode a location query.
153

154
        :param string query: The address or query you wish to geocode.
155

156
        :param bool exactly_one: Return one result or a list of results, if
157
            available.
158

159
        :param int timeout: Time, in seconds, to wait for the geocoding service
160
            to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
161
            exception. Set this only if you wish to override, on this call
162
            only, the value set during the geocoder's initialization.
163

164
            .. versionadded:: 0.97
165

166
        :param bounds: The bounding box of the viewport within which
167
            to bias geocode results more prominently.
168
        :type bounds: list or tuple
169

170
        :param string region: The region code, specified as a ccTLD
171
            ("top-level domain") two-character value.
172

173
        :param dict components: Restricts to an area. Can use any combination
174
            of: route, locality, administrative_area, postal_code, country.
175

176
            .. versionadded:: 0.97.1
177

178
        :param string language: The language in which to return results.
179

180
        :param bool sensor: Whether the geocoding request comes from a
181
            device with a location sensor.
182
        """
183
        params = {
184
            'address': self.format_string % query,
185
            'sensor': str(sensor).lower()
186
        }
187
        if self.api_key:
188
            params['key'] = self.api_key
189
        if bounds:
190
            params['bounds'] = bounds
191
        if region:
192
            params['region'] = region
193
        if components:
194
            params['components'] = self._format_components_param(components)
195
        if language:
196
            params['language'] = language
197

    
198
        if self.premier is False:
199
            url = "?".join((self.api, urlencode(params)))
200
        else:
201
            url = self._get_signed_url(params)
202

    
203
        logger.debug("%s.geocode: %s", self.__class__.__name__, url)
204
        return self._parse_json(
205
            self._call_geocoder(url, timeout=timeout), exactly_one
206
        )
207

    
208
    def reverse(
209
            self,
210
            query,
211
            exactly_one=False,
212
            timeout=None,
213
            language=None,
214
            sensor=False,
215
        ):  # pylint: disable=W0221,R0913
216
        """
217
        Given a point, find an address.
218

219
        :param query: The coordinates for which you wish to obtain the
220
            closest human-readable addresses.
221
        :type query: :class:`geopy.point.Point`, list or tuple of (latitude,
222
            longitude), or string as "%(latitude)s, %(longitude)s"
223

224
        :param boolean exactly_one: Return one result or a list of results, if
225
            available.
226

227
        :param int timeout: Time, in seconds, to wait for the geocoding service
228
            to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
229
            exception.
230

231
            .. versionadded:: 0.97
232

233
        :param string language: The language in which to return results.
234

235
        :param boolean sensor: Whether the geocoding request comes from a
236
            device with a location sensor.
237
        """
238
        params = {
239
            'latlng': self._coerce_point_to_string(query),
240
            'sensor': str(sensor).lower()
241
        }
242
        if language:
243
            params['language'] = language
244

    
245
        if not self.premier:
246
            url = "?".join((self.api, urlencode(params)))
247
        else:
248
            url = self._get_signed_url(params)
249

    
250
        logger.debug("%s.reverse: %s", self.__class__.__name__, url)
251
        return self._parse_json(
252
            self._call_geocoder(url, timeout=timeout), exactly_one
253
        )
254

    
255
    def timezone(self, location, at_time=None, timeout=None):
256
        """
257
        **This is an unstable API.**
258

259
        Finds the timezone a `location` was in for a specified `at_time`,
260
        and returns a pytz timezone object.
261

262
            .. versionadded:: 1.2.0
263

264
        :param location: The coordinates for which you want a timezone.
265
        :type location: :class:`geopy.point.Point`, list or tuple of (latitude,
266
            longitude), or string as "%(latitude)s, %(longitude)s"
267

268
        :param at_time: The time at which you want the timezone of this
269
            location. This is optional, and defaults to the time that the
270
            function is called in UTC.
271
        :type at_time integer, long, float, datetime:
272

273
        :rtype: pytz timezone
274
        """
275
        if not pytz_available:
276
            raise ImportError(
277
                'pytz must be installed in order to locate timezones. '
278
                ' Install with `pip install geopy -e ".[timezone]"`.'
279
            )
280
        location = self._coerce_point_to_string(location)
281

    
282
        if isinstance(at_time, Number):
283
            timestamp = at_time
284
        elif isinstance(at_time, datetime):
285
            timestamp = timegm(at_time.utctimetuple())
286
        elif at_time is None:
287
            timestamp = timegm(datetime.utcnow().utctimetuple())
288
        else:
289
            raise GeocoderQueryError(
290
                "`at_time` must be an epoch integer or "
291
                "datetime.datetime object"
292
            )
293

    
294
        params = {
295
            "location": location,
296
            "timestamp": timestamp,
297
        }
298
        url = "?".join((self.tz_api, urlencode(params)))
299

    
300
        logger.debug("%s.timezone: %s", self.__class__.__name__, url)
301
        response = self._call_geocoder(url, timeout=timeout)
302

    
303
        try:
304
            tz = timezone(response["timeZoneId"])
305
        except UnknownTimeZoneError:
306
            raise GeocoderParseError(
307
                "pytz could not parse the timezone identifier (%s) "
308
                "returned by the service." % response["timeZoneId"]
309
            )
310
        except KeyError:
311
            raise GeocoderParseError(
312
                "geopy could not find a timezone in this response: %s" %
313
                response
314
            )
315
        return tz
316

    
317
    def _parse_json(self, page, exactly_one=True):
318
        '''Returns location, (latitude, longitude) from json feed.'''
319

    
320
        places = page.get('results', [])
321
        if not len(places):
322
            self._check_status(page.get('status'))
323
            return None
324

    
325
        def parse_place(place):
326
            '''Get the location, lat, lng from a single json place.'''
327
            location = place.get('formatted_address')
328
            latitude = place['geometry']['location']['lat']
329
            longitude = place['geometry']['location']['lng']
330
            return Location(location, (latitude, longitude), place)
331

    
332
        if exactly_one:
333
            return parse_place(places[0])
334
        else:
335
            return [parse_place(place) for place in places]
336

    
337
    @staticmethod
338
    def _check_status(status):
339
        """
340
        Validates error statuses.
341
        """
342
        if status == 'ZERO_RESULTS':
343
            # When there are no results, just return.
344
            return
345
        if status == 'OVER_QUERY_LIMIT':
346
            raise GeocoderQuotaExceeded(
347
                'The given key has gone over the requests limit in the 24'
348
                ' hour period or has submitted too many requests in too'
349
                ' short a period of time.'
350
            )
351
        elif status == 'REQUEST_DENIED':
352
            raise GeocoderQueryError(
353
                'Your request was denied.'
354
            )
355
        elif status == 'INVALID_REQUEST':
356
            raise GeocoderQueryError('Probably missing address or latlng.')
357
        else:
358
            raise GeocoderQueryError('Unknown error.')
359