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 @ 564
History | View | Annotate | Download (12.5 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 |
user_agent=None,
|
49 |
): # pylint: disable=R0913
|
50 |
"""
|
51 |
Initialize a customized Google geocoder.
|
52 |
|
53 |
API authentication is only required for Google Maps Premier customers.
|
54 |
|
55 |
:param string api_key: The API key required by Google to perform
|
56 |
geocoding requests. API keys are managed through the Google APIs
|
57 |
console (https://code.google.com/apis/console).
|
58 |
|
59 |
.. versionadded:: 0.98.2
|
60 |
|
61 |
:param string domain: Should be the localized Google Maps domain to
|
62 |
connect to. The default is 'maps.googleapis.com', but if you're
|
63 |
geocoding address in the UK (for example), you may want to set it
|
64 |
to 'maps.google.co.uk' to properly bias results.
|
65 |
|
66 |
:param string scheme: Use 'https' or 'http' as the API URL's scheme.
|
67 |
Default is https. Note that SSL connections' certificates are not
|
68 |
verified.
|
69 |
|
70 |
.. versionadded:: 0.97
|
71 |
|
72 |
:param string client_id: If using premier, the account client id.
|
73 |
|
74 |
:param string secret_key: If using premier, the account secret key.
|
75 |
|
76 |
:param dict proxies: If specified, routes this geocoder's requests
|
77 |
through the specified proxy. E.g., {"https": "192.0.2.0"}. For
|
78 |
more information, see documentation on
|
79 |
:class:`urllib2.ProxyHandler`.
|
80 |
|
81 |
.. versionadded:: 0.96
|
82 |
"""
|
83 |
super(GoogleV3, self).__init__( |
84 |
scheme=scheme, timeout=timeout, proxies=proxies, user_agent=user_agent |
85 |
) |
86 |
if client_id and not secret_key: |
87 |
raise ConfigurationError('Must provide secret_key with client_id.') |
88 |
if secret_key and not client_id: |
89 |
raise ConfigurationError('Must provide client_id with secret_key.') |
90 |
|
91 |
self.api_key = api_key
|
92 |
self.domain = domain.strip('/') |
93 |
self.scheme = scheme
|
94 |
self.doc = {}
|
95 |
|
96 |
if client_id and secret_key: |
97 |
self.premier = True |
98 |
self.client_id = client_id
|
99 |
self.secret_key = secret_key
|
100 |
else:
|
101 |
self.premier = False |
102 |
self.client_id = None |
103 |
self.secret_key = None |
104 |
|
105 |
self.api = '%s://%s/maps/api/geocode/json' % (self.scheme, self.domain) |
106 |
self.tz_api = '%s://%s/maps/api/timezone/json' % ( |
107 |
self.scheme,
|
108 |
self.domain
|
109 |
) |
110 |
|
111 |
def _get_signed_url(self, params): |
112 |
"""
|
113 |
Returns a Premier account signed url. Docs on signature:
|
114 |
https://developers.google.com/maps/documentation/business/webservices/auth#digital_signatures
|
115 |
"""
|
116 |
params['client'] = self.client_id |
117 |
path = "?".join(('/maps/api/geocode/json', urlencode(params))) |
118 |
signature = hmac.new( |
119 |
base64.urlsafe_b64decode(self.secret_key),
|
120 |
path.encode('utf-8'),
|
121 |
hashlib.sha1 |
122 |
) |
123 |
signature = base64.urlsafe_b64encode( |
124 |
signature.digest() |
125 |
).decode('utf-8')
|
126 |
return '%s://%s%s&signature=%s' % ( |
127 |
self.scheme, self.domain, path, signature |
128 |
) |
129 |
|
130 |
@staticmethod
|
131 |
def _format_components_param(components): |
132 |
"""
|
133 |
Format the components dict to something Google understands.
|
134 |
"""
|
135 |
return "|".join( |
136 |
(":".join(item)
|
137 |
for item in components.items() |
138 |
) |
139 |
) |
140 |
|
141 |
@staticmethod
|
142 |
def _format_bounds_param(bounds): |
143 |
"""
|
144 |
Format the bounds to something Google understands.
|
145 |
"""
|
146 |
return '%f,%f|%f,%f' % (bounds[0], bounds[1], bounds[2], bounds[3]) |
147 |
|
148 |
def geocode( |
149 |
self,
|
150 |
query, |
151 |
exactly_one=True,
|
152 |
timeout=None,
|
153 |
bounds=None,
|
154 |
region=None,
|
155 |
components=None,
|
156 |
language=None,
|
157 |
sensor=False,
|
158 |
): # pylint: disable=W0221,R0913
|
159 |
"""
|
160 |
Geocode a location query.
|
161 |
|
162 |
:param string query: The address or query you wish to geocode.
|
163 |
|
164 |
:param bool exactly_one: Return one result or a list of results, if
|
165 |
available.
|
166 |
|
167 |
:param int timeout: Time, in seconds, to wait for the geocoding service
|
168 |
to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
|
169 |
exception. Set this only if you wish to override, on this call
|
170 |
only, the value set during the geocoder's initialization.
|
171 |
|
172 |
.. versionadded:: 0.97
|
173 |
|
174 |
:param bounds: The bounding box of the viewport within which
|
175 |
to bias geocode results more prominently.
|
176 |
:type bounds: list or tuple
|
177 |
|
178 |
:param string region: The region code, specified as a ccTLD
|
179 |
("top-level domain") two-character value.
|
180 |
|
181 |
:param dict components: Restricts to an area. Can use any combination
|
182 |
of: route, locality, administrative_area, postal_code, country.
|
183 |
|
184 |
.. versionadded:: 0.97.1
|
185 |
|
186 |
:param string language: The language in which to return results.
|
187 |
|
188 |
:param bool sensor: Whether the geocoding request comes from a
|
189 |
device with a location sensor.
|
190 |
"""
|
191 |
params = { |
192 |
'address': self.format_string % query, |
193 |
'sensor': str(sensor).lower() |
194 |
} |
195 |
if self.api_key: |
196 |
params['key'] = self.api_key |
197 |
if bounds:
|
198 |
if len(bounds) != 4: |
199 |
raise GeocoderQueryError(
|
200 |
"bounds must be a four-item iterable of lat,lon,lat,lon"
|
201 |
) |
202 |
params['bounds'] = self._format_bounds_param(bounds) |
203 |
if region:
|
204 |
params['region'] = region
|
205 |
if components:
|
206 |
params['components'] = self._format_components_param(components) |
207 |
if language:
|
208 |
params['language'] = language
|
209 |
|
210 |
if self.premier is False: |
211 |
url = "?".join((self.api, urlencode(params))) |
212 |
else:
|
213 |
url = self._get_signed_url(params)
|
214 |
|
215 |
logger.debug("%s.geocode: %s", self.__class__.__name__, url) |
216 |
return self._parse_json( |
217 |
self._call_geocoder(url, timeout=timeout), exactly_one
|
218 |
) |
219 |
|
220 |
def reverse( |
221 |
self,
|
222 |
query, |
223 |
exactly_one=False,
|
224 |
timeout=None,
|
225 |
language=None,
|
226 |
sensor=False,
|
227 |
): # pylint: disable=W0221,R0913
|
228 |
"""
|
229 |
Given a point, find an address.
|
230 |
|
231 |
:param query: The coordinates for which you wish to obtain the
|
232 |
closest human-readable addresses.
|
233 |
:type query: :class:`geopy.point.Point`, list or tuple of (latitude,
|
234 |
longitude), or string as "%(latitude)s, %(longitude)s"
|
235 |
|
236 |
:param boolean exactly_one: Return one result or a list of results, if
|
237 |
available.
|
238 |
|
239 |
:param int timeout: Time, in seconds, to wait for the geocoding service
|
240 |
to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
|
241 |
exception.
|
242 |
|
243 |
.. versionadded:: 0.97
|
244 |
|
245 |
:param string language: The language in which to return results.
|
246 |
|
247 |
:param boolean sensor: Whether the geocoding request comes from a
|
248 |
device with a location sensor.
|
249 |
"""
|
250 |
params = { |
251 |
'latlng': self._coerce_point_to_string(query), |
252 |
'sensor': str(sensor).lower() |
253 |
} |
254 |
if language:
|
255 |
params['language'] = language
|
256 |
if self.api_key: |
257 |
params['key'] = self.api_key |
258 |
|
259 |
if not self.premier: |
260 |
url = "?".join((self.api, urlencode(params))) |
261 |
else:
|
262 |
url = self._get_signed_url(params)
|
263 |
|
264 |
logger.debug("%s.reverse: %s", self.__class__.__name__, url) |
265 |
return self._parse_json( |
266 |
self._call_geocoder(url, timeout=timeout), exactly_one
|
267 |
) |
268 |
|
269 |
def timezone(self, location, at_time=None, timeout=None): |
270 |
"""
|
271 |
**This is an unstable API.**
|
272 |
|
273 |
Finds the timezone a `location` was in for a specified `at_time`,
|
274 |
and returns a pytz timezone object.
|
275 |
|
276 |
.. versionadded:: 1.2.0
|
277 |
|
278 |
:param location: The coordinates for which you want a timezone.
|
279 |
:type location: :class:`geopy.point.Point`, list or tuple of (latitude,
|
280 |
longitude), or string as "%(latitude)s, %(longitude)s"
|
281 |
|
282 |
:param at_time: The time at which you want the timezone of this
|
283 |
location. This is optional, and defaults to the time that the
|
284 |
function is called in UTC.
|
285 |
:type at_time integer, long, float, datetime:
|
286 |
|
287 |
:rtype: pytz timezone
|
288 |
"""
|
289 |
if not pytz_available: |
290 |
raise ImportError( |
291 |
'pytz must be installed in order to locate timezones. '
|
292 |
' Install with `pip install geopy -e ".[timezone]"`.'
|
293 |
) |
294 |
location = self._coerce_point_to_string(location)
|
295 |
|
296 |
if isinstance(at_time, Number): |
297 |
timestamp = at_time |
298 |
elif isinstance(at_time, datetime): |
299 |
timestamp = timegm(at_time.utctimetuple()) |
300 |
elif at_time is None: |
301 |
timestamp = timegm(datetime.utcnow().utctimetuple()) |
302 |
else:
|
303 |
raise GeocoderQueryError(
|
304 |
"`at_time` must be an epoch integer or "
|
305 |
"datetime.datetime object"
|
306 |
) |
307 |
|
308 |
params = { |
309 |
"location": location,
|
310 |
"timestamp": timestamp,
|
311 |
} |
312 |
if self.api_key: |
313 |
params['key'] = self.api_key |
314 |
url = "?".join((self.tz_api, urlencode(params))) |
315 |
|
316 |
logger.debug("%s.timezone: %s", self.__class__.__name__, url) |
317 |
response = self._call_geocoder(url, timeout=timeout)
|
318 |
|
319 |
try:
|
320 |
tz = timezone(response["timeZoneId"])
|
321 |
except UnknownTimeZoneError:
|
322 |
raise GeocoderParseError(
|
323 |
"pytz could not parse the timezone identifier (%s) "
|
324 |
"returned by the service." % response["timeZoneId"] |
325 |
) |
326 |
except KeyError: |
327 |
raise GeocoderParseError(
|
328 |
"geopy could not find a timezone in this response: %s" %
|
329 |
response |
330 |
) |
331 |
return tz
|
332 |
|
333 |
def _parse_json(self, page, exactly_one=True): |
334 |
'''Returns location, (latitude, longitude) from json feed.'''
|
335 |
|
336 |
places = page.get('results', [])
|
337 |
if not len(places): |
338 |
self._check_status(page.get('status')) |
339 |
return None |
340 |
|
341 |
def parse_place(place): |
342 |
'''Get the location, lat, lng from a single json place.'''
|
343 |
location = place.get('formatted_address')
|
344 |
latitude = place['geometry']['location']['lat'] |
345 |
longitude = place['geometry']['location']['lng'] |
346 |
return Location(location, (latitude, longitude), place)
|
347 |
|
348 |
if exactly_one:
|
349 |
return parse_place(places[0]) |
350 |
else:
|
351 |
return [parse_place(place) for place in places] |
352 |
|
353 |
@staticmethod
|
354 |
def _check_status(status): |
355 |
"""
|
356 |
Validates error statuses.
|
357 |
"""
|
358 |
if status == 'ZERO_RESULTS': |
359 |
# When there are no results, just return.
|
360 |
return
|
361 |
if status == 'OVER_QUERY_LIMIT': |
362 |
raise GeocoderQuotaExceeded(
|
363 |
'The given key has gone over the requests limit in the 24'
|
364 |
' hour period or has submitted too many requests in too'
|
365 |
' short a period of time.'
|
366 |
) |
367 |
elif status == 'REQUEST_DENIED': |
368 |
raise GeocoderQueryError(
|
369 |
'Your request was denied.'
|
370 |
) |
371 |
elif status == 'INVALID_REQUEST': |
372 |
raise GeocoderQueryError('Probably missing address or latlng.') |
373 |
else:
|
374 |
raise GeocoderQueryError('Unknown error.') |
375 |
|