• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mthh / routingpy / 19018276231

02 Nov 2025 09:17PM UTC coverage: 88.943%. First build
19018276231

Pull #150

github

web-flow
Merge e90838d9f into eb20b436a
Pull Request #150: feat: add geotiff support valhalla isochrones

9 of 9 new or added lines in 1 file covered. (100.0%)

1649 of 1854 relevant lines covered (88.94%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.72
/routingpy/routers/google.py
1
# -*- coding: utf-8 -*-
2
# Copyright (C) 2021 GIS OPS UG
3
#
4
#
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6
# use this file except in compliance with the License. You may obtain a copy of
7
# the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations under
15
# the License.
16
#
17

18
from operator import itemgetter
1✔
19
from typing import List, Optional, Tuple, Union
1✔
20

21
from .. import convert, utils
1✔
22
from ..client_base import DEFAULT
1✔
23
from ..client_default import Client
1✔
24
from ..direction import Direction, Directions
1✔
25
from ..exceptions import OverQueryLimit, RouterApiError, RouterServerError
1✔
26
from ..matrix import Matrix
1✔
27

28
STATUS_CODES = {
1✔
29
    "NOT_FOUND": {
30
        "code": 404,
31
        "message": "At least one of the locations specified in the request's origin, destination, or waypoints could not be geocoded.",
32
    },
33
    "ZERO_RESULTS": {
34
        "code": 404,
35
        "message": "No route could be found between the origin and destination.",
36
    },
37
    "MAX_WAYPOINTS_EXCEEDED": {
38
        "code": 413,
39
        "message": "Too many waypoints were provided in the request. The maximum is 25 excluding the origin and destination points.",
40
    },
41
    "MAX_ROUTE_LENGTH_EXCEEDED": {
42
        "code": 413,
43
        "message": "The requested route is too long and cannot be processed.",
44
    },
45
    "INVALID_REQUEST": {
46
        "code": 400,
47
        "message": "The provided request is invalid. Please check your parameters or parameter values.",
48
    },
49
    "OVER_DAILY_LIMIT": {
50
        "code": 429,
51
        "message": "This may be caused by an invalid API key, or billing issues.",
52
    },
53
    "OVER_QUERY_LIMIT": {
54
        "code": 429,
55
        "message": "The service has received too many requests from your application within the allowed time period.",
56
    },
57
    "REQUEST_DENIED": {
58
        "code": 403,
59
        "message": "The service denied use of the directions service by your application.",
60
    },
61
    "UNKNOWN_ERROR": {
62
        "code": 503,
63
        "message": "The directions request could not be processed due to a server error. The request may succeed if you try again.",
64
    },
65
}
66

67

68
class Google:
1✔
69
    """Performs requests to the Google API services."""
70

71
    _base_url = "https://maps.googleapis.com/maps/api"
1✔
72

73
    def __init__(
1✔
74
        self,
75
        api_key: str,
76
        user_agent: Optional[str] = None,
77
        timeout: Optional[int] = DEFAULT,
78
        retry_timeout: Optional[int] = None,
79
        retry_over_query_limit=True,
80
        skip_api_error: Optional[bool] = None,
81
        client=Client,
82
        **client_kwargs
83
    ):
84
        """
85
        Initializes a Google client.
86

87
        :param api_key: API key.
88
        :type api_key: str
89

90
        :param user_agent: User Agent to be used when requesting.
91
            Default :attr:`routingpy.routers.options.default_user_agent`.
92
        :type user_agent: str
93

94
        :param timeout: Combined connect and read timeout for HTTP requests, in
95
            seconds. Specify ``None`` for no timeout. Default :attr:`routingpy.routers.options.default_timeout`.
96
        :type timeout: int or None
97

98
        :param retry_timeout: Timeout across multiple retriable requests, in
99
            seconds.  Default :attr:`routingpy.routers.options.default_retry_timeout`.
100
        :type retry_timeout: int
101

102
        :param retry_over_query_limit: If True, client will not raise an exception
103
            on HTTP 429, but instead jitter a sleeping timer to pause between
104
            requests until HTTP 200 or retry_timeout is reached.
105
            Default :attr:`routingpy.routers.options.default_over_query_limit`.
106
        :type retry_over_query_limit: bool
107

108
        :param skip_api_error: Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
109
            encountered (e.g. no route found). If False, processing will discontinue and raise an error.
110
            Default :attr:`routingpy.routers.options.default_skip_api_error`.
111
        :type skip_api_error: bool
112

113
        :param client: A client class for request handling. Needs to be derived from :class:`routingpy.client_base.BaseClient`
114
        :type client: abc.ABCMeta
115

116
        :param client_kwargs: Additional arguments passed to the client, such as headers or proxies.
117
        :type client_kwargs: dict
118
        """
119

120
        self.key = api_key
1✔
121

122
        self.client = client(
1✔
123
            self._base_url,
124
            user_agent,
125
            timeout,
126
            retry_timeout,
127
            retry_over_query_limit,
128
            skip_api_error,
129
            **client_kwargs
130
        )
131

132
    class WayPoint(object):
1✔
133
        """
134
        TODO: make the WayPoint class and its parameters appear in Sphinx. True for Valhalla as well.
135

136
        Optionally construct a waypoint from this class with additional attributes.
137

138
        Example:
139

140
        >>> waypoint = Google.WayPoint(position=[8.15315, 52.53151], waypoint_type='coords', stopover=False)
141
        >>> route = Google(api_key).directions(locations=[[[8.58232, 51.57234]], waypoint, [7.15315, 53.632415]])
142
        """
143

144
        def __init__(self, position, waypoint_type="coords", stopover=True):
1✔
145
            """
146
            Constructs a waypoint with additional information, such as via or encoded lines.
147

148
            :param position: Coordinates in [long, lat] order.
149
            :type position: list/tuple of float
150

151
            :param waypoint_type: The type of information provided. One of ['place_id', 'enc', 'coords']. Default 'coords'.
152
            :type waypoint_type: str
153

154
            :param stopover: If True, the waypoint will be used to add an additional leg to the journey. If False,
155
                it's only used as a via waypoint. Not supported for first and last waypoint. Default True.
156
            :type stopover: bool
157
            """
158

159
            self.position = position
1✔
160
            self.waypoint_type = waypoint_type
1✔
161
            self.stopover = stopover
1✔
162

163
        def make_waypoint(self):
1✔
164
            waypoint = ""
1✔
165
            if self.waypoint_type == "coords":
1✔
166
                waypoint += convert.delimit_list(list(reversed(self.position)))
1✔
167
            elif self.waypoint_type == "place_id":
1✔
168
                waypoint += self.waypoint_type + ":" + self.position
1✔
169
            elif self.waypoint_type == "enc":
1✔
170
                waypoint += self.waypoint_type + ":" + self.position + ":"
1✔
171
            else:
172
                raise ValueError("waypoint_type only supports enc, place_id, coords")
1✔
173

174
            if not self.stopover:
1✔
175
                waypoint = "via:" + waypoint
1✔
176

177
            return waypoint
1✔
178

179
    def directions(  # noqa: C901
1✔
180
        self,
181
        locations: List[List[float]],
182
        profile: str,
183
        alternatives: Optional[bool] = None,
184
        avoid: Optional[List[str]] = None,
185
        optimize: Optional[bool] = None,
186
        language: Optional[str] = None,
187
        region: Optional[str] = None,
188
        units: Optional[str] = None,
189
        arrival_time: Optional[int] = None,
190
        departure_time: Optional[int] = None,
191
        traffic_model: Optional[str] = None,
192
        transit_mode: Optional[Union[List[str], Tuple[str]]] = None,
193
        transit_routing_preference: Optional[str] = None,
194
        dry_run: Optional[bool] = None,
195
    ):
196
        """Get directions between an origin point and a destination point.
197

198
        For more information, visit https://developers.google.com/maps/documentation/directions/overview.
199

200
        :param locations: The coordinates tuple the route should be calculated
201
            from in order of visit. Can be a list/tuple of [lon, lat], a list/tuple of address strings, Google's
202
            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].
203
            Optionally, specify ``optimize=true`` for via waypoint optimization.
204
        :type locations: list of list or list of :class:`Google.WayPoint`
205

206
        :param profile: The vehicle for which the route should be calculated.
207
            Default "driving". One of ['driving', 'walking', 'bicycling', 'transit'].
208
        :type profile: str
209

210
        :param alternatives: Specifies whether more than one route should be returned.
211
            Only available for requests without intermediate waypoints. Default False.
212
        :type alternatives: bool
213

214
        :param avoid: Indicates that the calculated route(s) should avoid the indicated features. One or more of
215
            ['tolls', 'highways', 'ferries', 'indoor']. Default None.
216
        :type avoid: list of str
217

218
        :param optimize: Optimize the given order of via waypoints (i.e. between first and last location). Default False.
219
        :type optimize: bool
220

221
        :param language: Language for routing instructions. The locale of the resulting turn instructions. Visit
222
            https://developers.google.com/maps/faq#languagesupport for options.
223
        :type language: str
224

225
        :param region: Specifies the region code, specified as a ccTLD ("top-level domain") two-character value.
226
            See https://developers.google.com/maps/documentation/directions/get-directions#RegionBiasing.
227
        :type region: str
228

229
        :param units: Specifies the unit system to use when displaying results. One of ['metric', 'imperial'].
230
        :type units: str
231

232
        :param arrival_time: Specifies the desired time of arrival for transit directions, in seconds since midnight,
233
            January 1, 1970 UTC. Incompatible with departure_time.
234
        :type arrival_time: int
235

236
        :param departure_time: Specifies the desired time of departure. You can specify the time as an integer in
237
            seconds since midnight, January 1, 1970 UTC.
238

239
        :param traffic_model: Specifies the assumptions to use when calculating time in traffic. One of ['best_guess',
240
            'pessimistic', 'optimistic'. See https://developers.google.com/maps/documentation/directions/get-directions#optional-parameters
241
            for details.
242
        :type traffic_model: str
243

244
        :param transit_mode: Specifies one or more preferred modes of transit. One or more of ['bus', 'subway', 'train',
245
            'tram', 'rail'].
246
        :type transit_mode: list/tuple of str
247

248
        :param transit_routing_preference: Specifies preferences for transit routes. Using this parameter, you can bias
249
            the options returned, rather than accepting the default best route chosen by the API. One of ['less_walking',
250
            'fewer_transfers'].
251
        :type transit_routing_preference: str
252

253
        :param dry_run: Print URL and parameters without sending the request.
254
        :type dry_run: bool
255

256
        :returns: One or multiple route(s) from provided coordinates and restrictions.
257
        :rtype: :class:`routingpy.direction.Direction` or :class:`routingpy.direction.Directions`
258
        """
259

260
        params = {"mode": profile}
1✔
261

262
        origin, destination = locations[0], locations[-1]
1✔
263
        if isinstance(origin, (list, tuple)):
1✔
264
            params["origin"] = convert.delimit_list(list(reversed(origin)))
1✔
265
        elif isinstance(origin, str):
×
266
            params["origin"] = origin
×
267
        elif isinstance(origin, self.WayPoint):
×
268
            raise TypeError("The first and last locations must be list/tuple of [lon, lat]")
×
269

270
        if isinstance(destination, (list, tuple)):
1✔
271
            params["destination"] = convert.delimit_list(list(reversed(destination)))
1✔
272
        elif isinstance(destination, str):
×
273
            params["destination"] = destination
×
274
        elif isinstance(origin, self.WayPoint):
×
275
            raise TypeError("The first and last locations must be list/tuple of [lon, lat]")
×
276

277
        if len(locations) > 2:
1✔
278
            waypoints = []
1✔
279
            s = slice(1, -1)
1✔
280
            for coord in locations[s]:
1✔
281
                if isinstance(coord, (list, tuple)):
1✔
282
                    waypoints.append(convert.delimit_list(list(reversed(coord))))
1✔
283
                elif isinstance(coord, self.WayPoint):
1✔
284
                    waypoints.append(coord.make_waypoint())
1✔
285
            if optimize:
1✔
286
                waypoints.insert(0, "optimize:true")
1✔
287

288
            params["waypoints"] = convert.delimit_list(waypoints, "|")
1✔
289

290
        if self.key is not None:
1✔
291
            params["key"] = self.key
1✔
292

293
        if alternatives is not None:
1✔
294
            params["alternatives"] = convert.convert_bool(alternatives)
1✔
295

296
        if avoid:
1✔
297
            params["avoid"] = convert.delimit_list(avoid, "|")
1✔
298

299
        if language:
1✔
300
            params["language"] = language
1✔
301

302
        if region:
1✔
303
            params["region"] = region
1✔
304

305
        if units:
1✔
306
            params["units"] = units
1✔
307

308
        if arrival_time and departure_time:
1✔
309
            raise ValueError("Specify either arrival_time or departure_time.")
×
310

311
        if arrival_time:
1✔
312
            params["arrival_time"] = str(arrival_time)
1✔
313

314
        if departure_time:
1✔
315
            params["departure_time"] = str(departure_time)
×
316

317
        if traffic_model:
1✔
318
            params["traffic_model"] = traffic_model
1✔
319

320
        if transit_mode:
1✔
321
            params["transit_mode"] = convert.delimit_list(transit_mode, "|")
1✔
322

323
        if transit_routing_preference:
1✔
324
            params["transit_routing_preference"] = transit_routing_preference
1✔
325

326
        return self.parse_direction_json(
1✔
327
            self.client._request("/directions/json", get_params=params, dry_run=dry_run), alternatives
328
        )
329

330
    @staticmethod
1✔
331
    def parse_direction_json(response, alternatives):
1✔
332
        if response is None:  # pragma: no cover
333
            if alternatives:
334
                return Directions()
335
            else:
336
                return Direction()
337

338
        status = response["status"]
1✔
339

340
        if status in STATUS_CODES.keys():
1✔
341
            if status == "UNKNOWN_ERROR":
1✔
342
                error = RouterServerError
1✔
343

344
            elif status in ["OVER_QUERY_LIMIT", "OVER_DAILY_LIMIT"]:
1✔
345
                error = OverQueryLimit
×
346

347
            else:
348
                error = RouterApiError
1✔
349

350
            raise error(STATUS_CODES[status]["code"], STATUS_CODES[status]["message"])
1✔
351

352
        if alternatives:
1✔
353
            routes = []
1✔
354
            for route in response["routes"]:
1✔
355
                geometry = []
1✔
356
                duration, distance = 0, 0
1✔
357
                for leg in route["legs"]:
1✔
358
                    duration += leg["duration"]["value"]
1✔
359
                    distance += leg["distance"]["value"]
1✔
360
                    for step in leg["steps"]:
1✔
361
                        geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
1✔
362

363
                routes.append(
1✔
364
                    Direction(
365
                        geometry=geometry, duration=int(duration), distance=int(distance), raw=route
366
                    )
367
                )
368
            return Directions(routes, response)
1✔
369
        else:
370
            geometry = []
1✔
371
            duration, distance = 0, 0
1✔
372
            for leg in response["routes"][0]["legs"]:
1✔
373
                duration += leg["duration"]["value"]
1✔
374
                distance += leg["distance"]["value"]
1✔
375
                for step in leg["steps"]:
1✔
376
                    geometry.extend(utils.decode_polyline5(step["polyline"]["points"]))
1✔
377

378
            return Direction(geometry=geometry, duration=duration, distance=distance, raw=response)
1✔
379

380
    def isochrones(self):  # pragma: no cover
381
        raise NotImplementedError
382

383
    def matrix(  # noqa: C901
1✔
384
        self,
385
        locations: List[List[float]],
386
        profile: str,
387
        sources: Optional[Union[List[int], Tuple[int]]] = None,
388
        destinations: Optional[Union[List[int], Tuple[int]]] = None,
389
        avoid: Optional[List[str]] = None,
390
        language: Optional[str] = None,
391
        region: Optional[str] = None,
392
        units: Optional[str] = None,
393
        arrival_time: Optional[int] = None,
394
        departure_time: Optional[int] = None,
395
        traffic_model: Optional[str] = None,
396
        transit_mode: Optional[Union[List[str], Tuple[str]]] = None,
397
        transit_routing_preference: Optional[str] = None,
398
        dry_run: Optional[bool] = None,
399
    ):
400
        """Gets travel distance and time for a matrix of origins and destinations.
401

402
        :param locations: Two or more pairs of lng/lat values.
403
        :type locations: list of list
404

405
        :param profile: The vehicle for which the route should be calculated.
406
            Default "driving". One of ['driving', 'walking', 'bicycling', 'transit'].
407
        :type profile: str
408

409
        :param sources: A list of indices that refer to the list of locations
410
            (starting with 0). If not passed, all indices are considered.
411
        :type sources: list or tuple
412

413
        :param destinations: A list of indices that refer to the list of locations
414
            (starting with 0). If not passed, all indices are considered.
415
        :type destinations: list or tuple
416

417
        :param avoid: Indicates that the calculated route(s) should avoid the indicated features. One or more of
418
            ['tolls', 'highways', 'ferries', 'indoor']. Default None.
419
        :param avoid: list of str
420

421
        :param language: Language for routing instructions. The locale of the resulting turn instructions. Visit
422
            https://developers.google.com/maps/faq#languagesupport for options.
423
        :type language: str
424

425
        :param region: Specifies the region code, specified as a ccTLD ("top-level domain") two-character value.
426
            See https://developers.google.com/maps/documentation/directions/get-directions#RegionBiasing.
427
        :type region: str
428

429
        :param units: Specifies the unit system to use when displaying results. One of ['metric', 'imperial'].
430
        :type units: str
431

432
        :param arrival_time: Specifies the desired time of arrival for transit directions, in seconds since midnight,
433
            January 1, 1970 UTC. Incompatible with departure_time.
434
        :type arrival_time: int
435

436
        :param departure_time: Specifies the desired time of departure. You can specify the time as an integer in
437
            seconds since midnight, January 1, 1970 UTC.
438
        :type departure_time: int
439

440
        :param traffic_model: Specifies the assumptions to use when calculating time in traffic. One of ['best_guess',
441
            'pessimistic', 'optimistic'. See https://developers.google.com/maps/documentation/directions/get-directions#optional-parameters
442
            for details.
443
        :type traffic_model: str
444

445
        :param transit_mode: Specifies one or more preferred modes of transit. One or more of ['bus', 'subway', 'train',
446
            'tram', 'rail'].
447
        :type transit_mode: list of str or tuple of str
448

449
        :param transit_routing_preference: Specifies preferences for transit routes. Using this parameter, you can bias
450
            the options returned, rather than accepting the default best route chosen by the API. One of ['less_walking',
451
            'fewer_transfers'].
452
        :type transit_routing_preference: str
453

454
        :param dry_run: Print URL and parameters without sending the request.
455
        :param dry_run: bool
456

457
        :returns: A matrix from the specified sources and destinations.
458
        :rtype: :class:`routingpy.matrix.Matrix`
459
        """
460
        params = {"mode": profile}
1✔
461

462
        waypoints = []
1✔
463
        for coord in locations:
1✔
464
            if isinstance(coord, (list, tuple)):
1✔
465
                waypoints.append(convert.delimit_list(list(reversed(coord))))
1✔
466
            elif isinstance(coord, self.WayPoint):
1✔
467
                waypoints.append(coord.make_waypoint())
1✔
468

469
        sources_coords = waypoints
1✔
470
        if sources is not None:
1✔
471
            sources_coords = itemgetter(*sources)(sources_coords)
1✔
472
            if not isinstance(sources_coords, (list, tuple)):
1✔
473
                sources_coords = [sources_coords]
1✔
474
        params["origins"] = convert.delimit_list(sources_coords, "|")
1✔
475

476
        destinations_coords = waypoints
1✔
477
        if destinations is not None:
1✔
478
            destinations_coords = itemgetter(*destinations)(destinations_coords)
1✔
479
            if not isinstance(destinations_coords, (list, tuple)):
1✔
480
                destinations_coords = [destinations_coords]
1✔
481
        params["destinations"] = convert.delimit_list(destinations_coords, "|")
1✔
482

483
        if self.key is not None:
1✔
484
            params["key"] = self.key
1✔
485

486
        if avoid:
1✔
487
            params["avoid"] = convert.delimit_list(avoid, "|")
1✔
488

489
        if language:
1✔
490
            params["language"] = language
1✔
491

492
        if region:
1✔
493
            params["region"] = region
1✔
494

495
        if units:
1✔
496
            params["units"] = units
1✔
497

498
        if arrival_time:
1✔
499
            params["arrival_time"] = str(arrival_time)
1✔
500

501
        if departure_time:
1✔
502
            params["departure_time"] = str(departure_time)
×
503

504
        if traffic_model:
1✔
505
            params["traffic_model"] = traffic_model
1✔
506

507
        if transit_mode:
1✔
508
            params["transit_mode"] = convert.delimit_list(transit_mode, "|")
1✔
509

510
        if transit_routing_preference:
1✔
511
            params["transit_routing_preference"] = transit_routing_preference
1✔
512

513
        return self.parse_matrix_json(
1✔
514
            self.client._request("/distancematrix/json", get_params=params, dry_run=dry_run)
515
        )
516

517
    @staticmethod
1✔
518
    def parse_matrix_json(response):
1✔
519
        if response is None:  # pragma: no cover
520
            return Matrix()
521

522
        durations = []
1✔
523
        distances = []
1✔
524
        for row in response["rows"]:
1✔
525
            row_durations = []
1✔
526
            row_distances = []
1✔
527
            for element in row["elements"]:
1✔
528
                if element["status"] == "OK":
1✔
529
                    row_durations.append(element["duration"]["value"])
1✔
530
                    row_distances.append(element["distance"]["value"])
1✔
531

532
                else:
533
                    row_durations.append(None)
×
534
                    row_distances.append(None)
×
535

536
            durations.append(row_durations)
1✔
537
            distances.append(row_distances)
1✔
538

539
        return Matrix(durations, distances, response)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc