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 |
|