-
Notifications
You must be signed in to change notification settings - Fork 25
/
google.py
535 lines (426 loc) 路 21.3 KB
/
google.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# -*- coding: utf-8 -*-
# Copyright (C) 2021 GIS OPS UG
#
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
from operator import itemgetter
from typing import List, Optional, Tuple, Union
from .. import convert, utils
from ..client_base import DEFAULT
from ..client_default import Client
from ..direction import Direction, Directions
from ..exceptions import OverQueryLimit, RouterApiError, RouterServerError
from ..matrix import Matrix
STATUS_CODES = {
"NOT_FOUND": {
"code": 404,
"message": "At least one of the locations specified in the request's origin, destination, or waypoints could not be geocoded.",
},
"ZERO_RESULTS": {
"code": 404,
"message": "No route could be found between the origin and destination.",
},
"MAX_WAYPOINTS_EXCEEDED": {
"code": 413,
"message": "Too many waypoints were provided in the request. The maximum is 25 excluding the origin and destination points.",
},
"MAX_ROUTE_LENGTH_EXCEEDED": {
"code": 413,
"message": "The requested route is too long and cannot be processed.",
},
"INVALID_REQUEST": {
"code": 400,
"message": "The provided request is invalid. Please check your parameters or parameter values.",
},
"OVER_DAILY_LIMIT": {
"code": 429,
"message": "This may be caused by an invalid API key, or billing issues.",
},
"OVER_QUERY_LIMIT": {
"code": 429,
"message": "The service has received too many requests from your application within the allowed time period.",
},
"REQUEST_DENIED": {
"code": 403,
"message": "The service denied use of the directions service by your application.",
},
"UNKNOWN_ERROR": {
"code": 503,
"message": "The directions request could not be processed due to a server error. The request may succeed if you try again.",
},
}
class Google:
"""Performs requests to the Google API services."""
_base_url = "https://maps.googleapis.com/maps/api"
def __init__(
self,
api_key: str,
user_agent: Optional[str] = None,
timeout: Optional[int] = DEFAULT,
retry_timeout: Optional[int] = None,
retry_over_query_limit=True,
skip_api_error: Optional[bool] = None,
client=Client,
**client_kwargs
):
"""
Initializes a Google client.
:param api_key: API key.
:type api_key: str
:param user_agent: User Agent to be used when requesting.
Default :attr:`routingpy.routers.options.default_user_agent`.
:type user_agent: str
:param timeout: Combined connect and read timeout for HTTP requests, in
seconds. Specify ``None`` for no timeout. Default :attr:`routingpy.routers.options.default_timeout`.
:type timeout: int or None
:param retry_timeout: Timeout across multiple retriable requests, in
seconds. Default :attr:`routingpy.routers.options.default_retry_timeout`.
:type retry_timeout: int
:param retry_over_query_limit: If True, client will not raise an exception
on HTTP 429, but instead jitter a sleeping timer to pause between
requests until HTTP 200 or retry_timeout is reached.
Default :attr:`routingpy.routers.options.default_over_query_limit`.
:type retry_over_query_limit: bool
:param skip_api_error: Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
encountered (e.g. no route found). If False, processing will discontinue and raise an error.
Default :attr:`routingpy.routers.options.default_skip_api_error`.
:type skip_api_error: bool
:param client: A client class for request handling. Needs to be derived from :class:`routingpy.client_base.BaseClient`
:type client: abc.ABCMeta
:param client_kwargs: Additional arguments passed to the client, such as headers or proxies.
:type client_kwargs: dict
"""
self.key = api_key
self.client = client(
self._base_url,
user_agent,
timeout,
retry_timeout,
retry_over_query_limit,
skip_api_error,
**client_kwargs
)
class WayPoint(object):
"""
TODO: make the WayPoint class and its parameters appear in Sphinx. True for Valhalla as well.
Optionally construct a waypoint from this class with additional attributes.
Example:
>>> waypoint = Google.WayPoint(position=[8.15315, 52.53151], waypoint_type='coords', stopover=False)
>>> route = Google(api_key).directions(locations=[[[8.58232, 51.57234]], waypoint, [7.15315, 53.632415]])
"""
def __init__(self, position, waypoint_type="coords", stopover=True):
"""
Constructs a waypoint with additional information, such as via or encoded lines.
:param position: Coordinates in [long, lat] order.
:type position: list/tuple of float
:param waypoint_type: The type of information provided. One of ['place_id', 'enc', 'coords']. Default 'coords'.
:type waypoint_type: str
:param stopover: If True, the waypoint will be used to add an additional leg to the journey. If False,
it's only used as a via waypoint. Not supported for first and last waypoint. Default True.
:type stopover: bool
"""
self.position = position
self.waypoint_type = waypoint_type
self.stopover = stopover
def make_waypoint(self):
waypoint = ""
if self.waypoint_type == "coords":
waypoint += convert.delimit_list(list(reversed(self.position)))
elif self.waypoint_type == "place_id":
waypoint += self.waypoint_type + ":" + self.position
elif self.waypoint_type == "enc":
waypoint += self.waypoint_type + ":" + self.position + ":"
else:
raise ValueError("waypoint_type only supports enc, place_id, coords")
if not self.stopover:
waypoint = "via:" + waypoint
return waypoint
def directions( # noqa: C901
self,
locations: List[List[float]],
profile: str,
alternatives: Optional[bool] = None,
avoid: Optional[List[str]] = None,
optimize: Optional[bool] = None,
language: Optional[str] = None,
region: Optional[str] = None,
units: Optional[str] = None,
arrival_time: Optional[int] = None,
departure_time: Optional[int] = None,
traffic_model: Optional[str] = None,
transit_mode: Optional[Union[List[str], Tuple[str]]] = None,
transit_routing_preference: Optional[str] = None,
dry_run: Optional[bool] = None,
):
"""Get directions between an origin point and a destination point.
For more information, visit https://developers.google.com/maps/documentation/directions/intro.
:param locations: The coordinates tuple the route should be calculated
from in order of visit. Can be a list/tuple of [lon, lat], a list/tuple of address strings, Google's
Place ID's, a :class:`Google.WayPoint` instance or a combination of these. Note, the first and last location have to be specified as [lon, lat].
Optionally, specify ``optimize=true`` for via waypoint optimization.
:type locations: list of list or list of :class:`Google.WayPoint`
:param profile: The vehicle for which the route should be calculated.
Default "driving". One of ['driving', 'walking', 'bicycling', 'transit'].
:type profile: str
:param alternatives: Specifies whether more than one route should be returned.
Only available for requests without intermediate waypoints. Default False.
:type alternatives: bool
:param avoid: Indicates that the calculated route(s) should avoid the indicated features. One or more of
['tolls', 'highways', 'ferries', 'indoor']. Default None.
:type avoid: list of str
:param optimize: Optimize the given order of via waypoints (i.e. between first and last location). Default False.
:type optimize: bool
:param language: Language for routing instructions. The locale of the resulting turn instructions. Visit
https://developers.google.com/maps/faq#languagesupport for options.
:type language: str
:param region: Specifies the region code, specified as a ccTLD ("top-level domain") two-character value.
See https://developers.google.com/maps/documentation/directions/intro#RegionBiasing.
:type region: str
:param units: Specifies the unit system to use when displaying results. One of ['metric', 'imperial'].
:type units: str
:param arrival_time: Specifies the desired time of arrival for transit directions, in seconds since midnight,
January 1, 1970 UTC. Incompatible with departure_time.
:type arrival_time: int
:param departure_time: Specifies the desired time of departure. You can specify the time as an integer in
seconds since midnight, January 1, 1970 UTC.
:param traffic_model: Specifies the assumptions to use when calculating time in traffic. One of ['best_guess',
'pessimistic', 'optimistic'. See https://developers.google.com/maps/documentation/directions/intro#optional-parameters
for details.
:type traffic_model: str
:param transit_mode: Specifies one or more preferred modes of transit. One or more of ['bus', 'subway', 'train',
'tram', 'rail'].
:type transit_mode: list/tuple of str
:param transit_routing_preference: Specifies preferences for transit routes. Using this parameter, you can bias
the options returned, rather than accepting the default best route chosen by the API. One of ['less_walking',
'fewer_transfers'].
:type transit_routing_preference: str
:param dry_run: Print URL and parameters without sending the request.
:type dry_run: bool
:returns: One or multiple route(s) from provided coordinates and restrictions.
:rtype: :class:`routingpy.direction.Direction` or :class:`routingpy.direction.Directions`
"""
params = {"mode": profile}
origin, destination = locations[0], locations[-1]
if isinstance(origin, (list, tuple)):
params["origin"] = convert.delimit_list(list(reversed(origin)))
elif isinstance(origin, self.WayPoint):
raise TypeError("The first and last locations must be list/tuple of [lon, lat]")
if isinstance(destination, (list, tuple)):
params["destination"] = convert.delimit_list(list(reversed(destination)))
elif isinstance(origin, self.WayPoint):
raise TypeError("The first and last locations must be list/tuple of [lon, lat]")
if len(locations) > 2:
waypoints = []
s = slice(1, -1)
for coord in locations[s]:
if isinstance(coord, (list, tuple)):
waypoints.append(convert.delimit_list(list(reversed(coord))))
elif isinstance(coord, self.WayPoint):
waypoints.append(coord.make_waypoint())
if optimize:
waypoints.insert(0, "optimize:true")
params["waypoints"] = convert.delimit_list(waypoints, "|")
if self.key is not None:
params["key"] = self.key
if alternatives is not None:
params["alternatives"] = convert.convert_bool(alternatives)
if avoid:
params["avoid"] = convert.delimit_list(avoid, "|")
if language:
params["language"] = language
if region:
params["region"] = region
if units:
params["units"] = units
if arrival_time and departure_time:
raise ValueError("Specify either arrival_time or departure_time.")
if arrival_time:
params["arrival_time"] = str(arrival_time)
if departure_time:
params["departure_time"] = str(departure_time)
if traffic_model:
params["traffic_model"] = traffic_model
if transit_mode:
params["transit_mode"] = convert.delimit_list(transit_mode, "|")
if transit_routing_preference:
params["transit_routing_preference"] = transit_routing_preference
return self.parse_direction_json(
self.client._request("/directions/json", get_params=params, dry_run=dry_run), alternatives
)
@staticmethod
def parse_direction_json(response, alternatives):
if response is None: # pragma: no cover
if alternatives:
return Directions()
else:
return Direction()
status = response["status"]
if status in STATUS_CODES.keys():
if status == "UNKNOWN_ERROR":
error = RouterServerError
elif status in ["OVER_QUERY_LIMIT", "OVER_DAILY_LIMIT"]:
error = OverQueryLimit
else:
error = RouterApiError
raise error(STATUS_CODES[status]["code"], STATUS_CODES[status]["message"])
if alternatives:
routes = []
for route in response["routes"]:
geometry = []
duration, distance = 0, 0
for leg in route["legs"]:
duration += leg["duration"]["value"]
distance += leg["distance"]["value"]
for step in leg["steps"]:
geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
routes.append(
Direction(
geometry=geometry, duration=int(duration), distance=int(distance), raw=route
)
)
return Directions(routes, response)
else:
geometry = []
duration, distance = 0, 0
for leg in response["routes"][0]["legs"]:
duration += leg["duration"]["value"]
distance += leg["distance"]["value"]
for step in leg["steps"]:
geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
return Direction(geometry=geometry, duration=duration, distance=distance, raw=response)
def isochrones(self): # pragma: no cover
raise NotImplementedError
def matrix( # noqa: C901
self,
locations: List[List[float]],
profile: str,
sources: Optional[Union[List[int], Tuple[int]]] = None,
destinations: Optional[Union[List[int], Tuple[int]]] = None,
avoid: Optional[List[str]] = None,
language: Optional[str] = None,
region: Optional[str] = None,
units: Optional[str] = None,
arrival_time: Optional[int] = None,
departure_time: Optional[int] = None,
traffic_model: Optional[str] = None,
transit_mode: Optional[Union[List[str], Tuple[str]]] = None,
transit_routing_preference: Optional[str] = None,
dry_run: Optional[bool] = None,
):
"""Gets travel distance and time for a matrix of origins and destinations.
:param locations: Two or more pairs of lng/lat values.
:type locations: list of list
:param profile: The vehicle for which the route should be calculated.
Default "driving". One of ['driving', 'walking', 'bicycling', 'transit'].
:type profile: str
:param sources: A list of indices that refer to the list of locations
(starting with 0). If not passed, all indices are considered.
:type sources: list or tuple
:param destinations: A list of indices that refer to the list of locations
(starting with 0). If not passed, all indices are considered.
:type destinations: list or tuple
:param avoid: Indicates that the calculated route(s) should avoid the indicated features. One or more of
['tolls', 'highways', 'ferries', 'indoor']. Default None.
:param avoid: list of str
:param language: Language for routing instructions. The locale of the resulting turn instructions. Visit
https://developers.google.com/maps/faq#languagesupport for options.
:type language: str
:param region: Specifies the region code, specified as a ccTLD ("top-level domain") two-character value.
See https://developers.google.com/maps/documentation/directions/intro#RegionBiasing.
:type region: str
:param units: Specifies the unit system to use when displaying results. One of ['metric', 'imperial'].
:type units: str
:param arrival_time: Specifies the desired time of arrival for transit directions, in seconds since midnight,
January 1, 1970 UTC. Incompatible with departure_time.
:type arrival_time: int
:param departure_time: Specifies the desired time of departure. You can specify the time as an integer in
seconds since midnight, January 1, 1970 UTC.
:type departure_time: int
:param traffic_model: Specifies the assumptions to use when calculating time in traffic. One of ['best_guess',
'pessimistic', 'optimistic'. See https://developers.google.com/maps/documentation/directions/intro#optional-parameters
for details.
:type traffic_model: str
:param transit_mode: Specifies one or more preferred modes of transit. One or more of ['bus', 'subway', 'train',
'tram', 'rail'].
:type transit_mode: list of str or tuple of str
:param transit_routing_preference: Specifies preferences for transit routes. Using this parameter, you can bias
the options returned, rather than accepting the default best route chosen by the API. One of ['less_walking',
'fewer_transfers'].
:type transit_routing_preference: str
:param dry_run: Print URL and parameters without sending the request.
:param dry_run: bool
:returns: A matrix from the specified sources and destinations.
:rtype: :class:`routingpy.matrix.Matrix`
"""
params = {"mode": profile}
waypoints = []
for coord in locations:
if isinstance(coord, (list, tuple)):
waypoints.append(convert.delimit_list(list(reversed(coord))))
elif isinstance(coord, self.WayPoint):
waypoints.append(coord.make_waypoint())
sources_coords = waypoints
if sources is not None:
sources_coords = itemgetter(*sources)(sources_coords)
if not isinstance(sources_coords, (list, tuple)):
sources_coords = [sources_coords]
params["origins"] = convert.delimit_list(sources_coords, "|")
destinations_coords = waypoints
if destinations is not None:
destinations_coords = itemgetter(*destinations)(destinations_coords)
if not isinstance(destinations_coords, (list, tuple)):
destinations_coords = [destinations_coords]
params["destinations"] = convert.delimit_list(destinations_coords, "|")
if self.key is not None:
params["key"] = self.key
if avoid:
params["avoid"] = convert.delimit_list(avoid, "|")
if language:
params["language"] = language
if region:
params["region"] = region
if units:
params["units"] = units
if arrival_time:
params["arrival_time"] = str(arrival_time)
if departure_time:
params["departure_time"] = str(departure_time)
if traffic_model:
params["traffic_model"] = traffic_model
if transit_mode:
params["transit_mode"] = convert.delimit_list(transit_mode, "|")
if transit_routing_preference:
params["transit_routing_preference"] = transit_routing_preference
return self.parse_matrix_json(
self.client._request("/distancematrix/json", get_params=params, dry_run=dry_run)
)
@staticmethod
def parse_matrix_json(response):
if response is None: # pragma: no cover
return Matrix()
durations = []
distances = []
for row in response["rows"]:
row_durations = []
row_distances = []
for element in row["elements"]:
if element["status"] == "OK":
row_durations.append(element["duration"]["value"])
row_distances.append(element["distance"]["value"])
else:
row_durations.append(None)
row_distances.append(None)
durations.append(row_durations)
distances.append(row_distances)
return Matrix(durations, distances, response)