r5py 0.1.2__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of r5py might be problematic. Click here for more details.

r5py/__init__.py CHANGED
@@ -2,22 +2,28 @@
2
2
 
3
3
  """Python wrapper for the R5 routing analysis engine."""
4
4
 
5
- __version__ = "0.1.2"
5
+ __version__ = "1.0.0"
6
6
 
7
7
 
8
8
  from .r5 import (
9
+ DetailedItineraries,
9
10
  DetailedItinerariesComputer,
11
+ Isochrones,
10
12
  RegionalTask,
11
13
  TransportMode,
12
14
  TransportNetwork,
15
+ TravelTimeMatrix,
13
16
  TravelTimeMatrixComputer,
14
17
  )
15
18
 
16
19
  __all__ = [
20
+ "DetailedItineraries",
17
21
  "DetailedItinerariesComputer",
22
+ "Isochrones",
18
23
  "RegionalTask",
19
24
  "TransportMode",
20
25
  "TransportNetwork",
26
+ "TravelTimeMatrix",
21
27
  "TravelTimeMatrixComputer",
22
28
  "__version__",
23
29
  ]
r5py/r5/__init__.py CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  from .access_leg import AccessLeg
6
6
  from .breakdown_stat import BreakdownStat
7
- from .detailed_itineraries_computer import DetailedItinerariesComputer
7
+ from .detailed_itineraries import DetailedItineraries, DetailedItinerariesComputer
8
8
  from .direct_leg import DirectLeg
9
9
  from .egress_leg import EgressLeg
10
+ from .isochrones import Isochrones
10
11
  from .regional_task import RegionalTask
11
12
  from .scenario import Scenario
12
13
  from .street_layer import StreetLayer
@@ -14,16 +15,18 @@ from .transfer_leg import TransferLeg
14
15
  from .transit_leg import TransitLeg
15
16
  from .transport_mode import TransportMode
16
17
  from .transport_network import TransportNetwork
17
- from .travel_time_matrix_computer import TravelTimeMatrixComputer
18
+ from .travel_time_matrix import TravelTimeMatrix, TravelTimeMatrixComputer
18
19
  from .trip import Trip
19
20
  from .trip_planner import TripPlanner
20
21
 
21
22
  __all__ = [
22
23
  "AccessLeg",
23
24
  "BreakdownStat",
25
+ "DetailedItineraries",
24
26
  "DetailedItinerariesComputer",
25
27
  "DirectLeg",
26
28
  "EgressLeg",
29
+ "Isochrones",
27
30
  "RegionalTask",
28
31
  "Scenario",
29
32
  "SpeedConfig",
@@ -32,6 +35,7 @@ __all__ = [
32
35
  "TransitLeg",
33
36
  "TransportMode",
34
37
  "TransportNetwork",
38
+ "TravelTimeMatrix",
35
39
  "TravelTimeMatrixComputer",
36
40
  "Trip",
37
41
  "TripPlanner",
@@ -6,6 +6,7 @@ import math
6
6
  import multiprocessing
7
7
  import warnings
8
8
 
9
+ import geopandas
9
10
  import numpy
10
11
  import shapely
11
12
 
@@ -14,7 +15,7 @@ from .regional_task import RegionalTask
14
15
  from .transport_network import TransportNetwork
15
16
 
16
17
 
17
- __all__ = ["BaseTravelTimeMatrixComputer"]
18
+ __all__ = ["BaseTravelTimeMatrix"]
18
19
 
19
20
 
20
21
  # R5 fills cut-off (NULL) values with MAX_INT32
@@ -26,13 +27,26 @@ MAX_INT32 = (2**31) - 1
26
27
  NUM_THREADS = math.ceil(multiprocessing.cpu_count() * 0.5)
27
28
 
28
29
 
29
- class BaseTravelTimeMatrixComputer:
30
+ class BaseTravelTimeMatrix(geopandas.GeoDataFrame):
30
31
  """Base class for travel time computers between many origins and destinations."""
31
32
 
32
33
  MAX_INT32 = MAX_INT32
33
34
 
34
35
  NUM_THREADS = NUM_THREADS
35
36
 
37
+ _r5py_attributes = [
38
+ "_destinations",
39
+ "_destinations_crs",
40
+ "_origins",
41
+ "_origins_crs",
42
+ "destinations",
43
+ "origins",
44
+ "request",
45
+ "snap_to_network",
46
+ "transport_network",
47
+ "verbose",
48
+ ]
49
+
36
50
  def __init__(
37
51
  self,
38
52
  transport_network,
@@ -71,6 +85,8 @@ class BaseTravelTimeMatrixComputer:
71
85
  ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
72
86
  ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
73
87
  """
88
+ geopandas.GeoDataFrame.__init__(self)
89
+
74
90
  if not isinstance(transport_network, TransportNetwork):
75
91
  transport_network = TransportNetwork(*transport_network)
76
92
  self.transport_network = transport_network
@@ -89,6 +105,13 @@ class BaseTravelTimeMatrixComputer:
89
105
 
90
106
  self.verbose = Config().arguments.verbose
91
107
 
108
+ def __setattr__(self, attr, val):
109
+ """Catch our own attributes here so we don’t mess with (geo)pandas columns."""
110
+ if attr in self._r5py_attributes:
111
+ object.__setattr__(self, attr, val)
112
+ else:
113
+ super().__setattr__(attr, val)
114
+
92
115
  @property
93
116
  def destinations(self):
94
117
  """The destinations of this travel time matrix (`geopandas.GeoDataFrame`)."""
@@ -159,8 +182,6 @@ class BaseTravelTimeMatrixComputer:
159
182
 
160
183
  setattr(self, f"_{which_end}", points.copy())
161
184
 
162
- self.snap_to_network = False # prevent repeated snapping on same point sets
163
-
164
185
  @property
165
186
  def origins(self):
166
187
  """The origins of this travel time matrix (`geopandas.GeoDataFrame`)."""
@@ -7,23 +7,33 @@
7
7
  import copy
8
8
  import warnings
9
9
 
10
+ try:
11
+ from warnings import deprecated
12
+ except ImportError: # Python<=3.12
13
+ from typing_extensions import deprecated
14
+
10
15
  import geopandas
11
16
  import joblib
12
17
  import pandas
13
18
 
14
- from .base_travel_time_matrix_computer import BaseTravelTimeMatrixComputer
19
+ from .base_travel_time_matrix import BaseTravelTimeMatrix
15
20
  from .trip import Trip
16
21
  from .trip_planner import ACCURATE_GEOMETRIES, TripPlanner
17
22
 
18
23
 
19
- __all__ = ["DetailedItinerariesComputer"]
24
+ __all__ = ["DetailedItineraries", "DetailedItinerariesComputer"]
20
25
 
21
26
 
22
- class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
27
+ class DetailedItineraries(BaseTravelTimeMatrix):
23
28
  """Compute detailed itineraries between many origins and destinations."""
24
29
 
25
30
  COLUMNS = ["from_id", "to_id", "option"] + Trip.COLUMNS
26
31
 
32
+ _r5py_attributes = BaseTravelTimeMatrix._r5py_attributes + [
33
+ "all_to_all",
34
+ "od_pairs",
35
+ ]
36
+
27
37
  def __init__(
28
38
  self,
29
39
  transport_network,
@@ -36,6 +46,10 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
36
46
  """
37
47
  Compute travel times between many origins and destinations.
38
48
 
49
+ ``r5py.DetailedItineraries`` are child classes of
50
+ ``geopandas.GeoDataFrame`` and support all of their methods and
51
+ properties, see https://geopandas.org/en/stable/docs.html
52
+
39
53
  Arguments
40
54
  ---------
41
55
  transport_network : r5py.TransportNetwork | tuple(str, list(str), dict)
@@ -58,10 +72,10 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
58
72
  if `int`, use `snap_to_network` meters as the search radius.
59
73
  force_all_to_all : bool, default False
60
74
  If ``origins`` and ``destinations`` have the same length, by
61
- default, ``DetailedItinerariesComputer`` finds routes between pairs
75
+ default, ``DetailedItineraries`` finds routes between pairs
62
76
  of origins and destinations, i.e., it routes from origin #1 to
63
77
  destination #1, origin #2 to destination #2, ... .
64
- Set ``all_to_all=True`` to route from each origin to all
78
+ Set ``force_all_to_all=True`` to route from each origin to all
65
79
  destinations (this is the default, if ``origins`` and ``destinations``
66
80
  have different lengths, or if ``destinations`` is omitted)
67
81
  **kwargs : mixed
@@ -70,7 +84,7 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
70
84
  ``access_modes``, ``egress_modes``, ``max_time``, ``max_time_walking``,
71
85
  ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
72
86
  ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
73
- Not that not all arguments might make sense in this context, and the
87
+ Note that not all arguments might make sense in this context, and the
74
88
  underlying R5 engine might ignore some of them.
75
89
  """
76
90
  super().__init__(
@@ -106,7 +120,23 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
106
120
  else:
107
121
  self.all_to_all = force_all_to_all
108
122
 
109
- def compute_travel_details(self):
123
+ data = self._compute()
124
+ with warnings.catch_warnings():
125
+ warnings.filterwarnings(
126
+ "ignore",
127
+ message=(
128
+ "You are adding a column named 'geometry' to a GeoDataFrame "
129
+ "constructed without an active geometry column"
130
+ ),
131
+ category=FutureWarning,
132
+ )
133
+ for column in data.columns:
134
+ self[column] = data[column]
135
+ self.set_geometry("geometry")
136
+
137
+ del self.transport_network
138
+
139
+ def _compute(self):
110
140
  """
111
141
  Compute travel times from all origins to all destinations.
112
142
 
@@ -114,15 +144,20 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
114
144
  -------
115
145
  geopandas.GeoDataFrame
116
146
  The resulting detailed routes. For each origin/destination pair,
147
+ multiple route alternatives (‘options’) might be reported that each
148
+ consist of one or more segments. Each segment represents one row.
117
149
  multiple route alternatives (‘options’) might be reported that each consist of
118
150
  one or more segments. Each segment represents one row.
151
+
119
152
  The data frame comprises of the following columns: `from_id`,
120
153
  `to_id`, `option` (`int`), `segment` (`int`), `transport_mode`
121
154
  (`r5py.TransportMode`), `departure_time` (`datetime.datetime`),
122
155
  `distance` (`float`, metres), `travel_time` (`datetime.timedelta`),
123
- `wait_time` (`datetime.timedelta`), `route` (`str`, public transport
124
- route number or name), `geometry` (`shapely.LineString`)
125
- TODO: Add description of output data frame columns and format
156
+ `wait_time` (`datetime.timedelta`), `feed` (`str`, the feed name
157
+ used), `agency_id` (`str` the public transport agency identifier),
158
+ `route_id` (`str`, public transport route ID), `start_stop_id`
159
+ (`str`, the GTFS stop_id for boarding), `end_stop_id` (`str`, the
160
+ GTFS stop_id for alighting), `geometry` (`shapely.LineString`)
126
161
  """
127
162
  self._prepare_origins_destinations()
128
163
 
@@ -204,3 +239,34 @@ class DetailedItinerariesComputer(BaseTravelTimeMatrixComputer):
204
239
  # fmt: on
205
240
 
206
241
  return pandas.DataFrame(trips, columns=self.COLUMNS)
242
+
243
+
244
+ @deprecated(
245
+ "Use `DetailedItineraries` instead, `DetailedItinerariesComputer will be deprecated in a future release."
246
+ )
247
+ class DetailedItinerariesComputer:
248
+ """Compute detailed itineraries between many origins and destinations."""
249
+
250
+ def __init__(self, *args, **kwargs):
251
+ """Compute detailed itineraries between many origins and destinations."""
252
+ self._detailed_itineraries = DetailedItineraries(*args, **kwargs)
253
+
254
+ def compute_travel_details(self):
255
+ """
256
+ Compute travel times from all origins to all destinations.
257
+
258
+ Returns
259
+ -------
260
+ geopandas.GeoDataFrame
261
+ The resulting detailed routes. For each origin/destination pair,
262
+ multiple route alternatives (‘options’) might be reported that each consist of
263
+ one or more segments. Each segment represents one row.
264
+ The data frame comprises of the following columns: `from_id`,
265
+ `to_id`, `option` (`int`), `segment` (`int`), `transport_mode`
266
+ (`r5py.TransportMode`), `departure_time` (`datetime.datetime`),
267
+ `distance` (`float`, metres), `travel_time` (`datetime.timedelta`),
268
+ `wait_time` (`datetime.timedelta`), `route` (`str`, public transport
269
+ route number or name), `geometry` (`shapely.LineString`)
270
+ TODO: Add description of output data frame columns and format
271
+ """
272
+ return self._detailed_itineraries
r5py/r5/isochrones.py ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """Compute polygons of equal travel time from a destination."""
5
+
6
+
7
+ import datetime
8
+ import warnings
9
+
10
+ import geohexgrid
11
+ import geopandas
12
+ import pandas
13
+ import pyproj
14
+ import shapely
15
+ import simplification.cutil
16
+
17
+ from .base_travel_time_matrix import BaseTravelTimeMatrix
18
+ from .transport_mode import TransportMode
19
+ from .travel_time_matrix import TravelTimeMatrix
20
+ from ..util import GoodEnoughEquidistantCrs, SpatiallyClusteredGeoDataFrame
21
+
22
+
23
+ __all__ = ["Isochrones"]
24
+
25
+
26
+ EMPTY_POINT = shapely.Point()
27
+ R5_CRS = "EPSG:4326"
28
+
29
+ CONCAVE_HULL_BUFFER_SIZE = 20.0 # metres
30
+ CONCAVE_HULL_RATIO = 0.3
31
+
32
+ VERY_SMALL_BUFFER_SIZE = 0.001 # turn points into polygons
33
+
34
+
35
+ class Isochrones(BaseTravelTimeMatrix):
36
+ """Compute polygons of equal travel time from a destination."""
37
+
38
+ _r5py_attributes = BaseTravelTimeMatrix._r5py_attributes + [
39
+ "_isochrones",
40
+ "isochrones",
41
+ "point_grid_resolution",
42
+ "point_grid_sample_ratio",
43
+ ]
44
+
45
+ def __init__(
46
+ self,
47
+ transport_network,
48
+ origins,
49
+ isochrones=pandas.timedelta_range(
50
+ start=datetime.timedelta(minutes=0),
51
+ end=datetime.timedelta(hours=1),
52
+ freq=datetime.timedelta(minutes=15),
53
+ ),
54
+ point_grid_resolution=100,
55
+ point_grid_sample_ratio=1.0,
56
+ **kwargs,
57
+ ):
58
+ """
59
+ Compute polygons of equal travel time from one or more destinations.
60
+
61
+ ``r5py.Isochrones`` are child classes of ``geopandas.GeoDataFrame`` and
62
+ support all of their methods and properties, see
63
+ https://geopandas.org/en/stable/docs.html
64
+
65
+ Arguments
66
+ ---------
67
+ transport_network : r5py.TransportNetwork | tuple(str, list(str), dict)
68
+ The transport network to route on. This can either be a readily
69
+ initialised r5py.TransportNetwork or a tuple of the parameters
70
+ passed to ``TransportNetwork.__init__()``: the path to an OpenStreetMap
71
+ extract in PBF format, a list of zero of more paths to GTFS transport
72
+ schedule files, and a dict with ``build_config`` options.
73
+ origins : geopandas.GeoDataFrame | shapely.Point
74
+ Place(s) to find a route _from_
75
+ Must be/have a point geometry. If multiple origin points are passed,
76
+ isochrones will be computed as minimum travel time from any of them.
77
+ isochrones : pandas.TimedeltaIndex | collections.abc.Iterable[int]
78
+ For which interval to compute isochrone polygons. An iterable of
79
+ integers is interpreted as minutes.
80
+ point_grid_resolution : int
81
+ Distance in meters between points in the regular grid of points laid over the
82
+ transport network’s extent that is used to compute isochrones.
83
+ Increase this value for performance, decrease it for precision.
84
+ point_grid_sample_ratio : float
85
+ Share of points of the point grid that are used in computation,
86
+ ranging from 0.01 to 1.0.
87
+ Increase this value for performance, decrease it for precision.
88
+ **kwargs : mixed
89
+ Any arguments than can be passed to r5py.RegionalTask:
90
+ ``departure``, ``departure_time_window``, ``percentiles``, ``transport_modes``,
91
+ ``access_modes``, ``egress_modes``, ``max_time``, ``max_time_walking``,
92
+ ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
93
+ ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
94
+ Note that not all arguments might make sense in this context, and the
95
+ underlying R5 engine might ignore some of them.
96
+ If percentiles are specified, the lowest one will be used for
97
+ isochrone computation.
98
+ """
99
+ geopandas.GeoDataFrame.__init__(self)
100
+ BaseTravelTimeMatrix.__init__(
101
+ self,
102
+ transport_network,
103
+ **kwargs,
104
+ )
105
+
106
+ self.EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(self.transport_network.extent)
107
+
108
+ if isinstance(origins, shapely.Geometry):
109
+ origins = geopandas.GeoDataFrame(
110
+ {
111
+ "id": [
112
+ "origin",
113
+ ],
114
+ "geometry": [
115
+ origins,
116
+ ],
117
+ },
118
+ crs=R5_CRS,
119
+ )
120
+ self.origins = origins
121
+ self.isochrones = isochrones
122
+
123
+ self.point_grid_resolution = point_grid_resolution
124
+ self.point_grid_sample_ratio = max(0.01, min(1.0, point_grid_sample_ratio))
125
+
126
+ travel_times = TravelTimeMatrix(
127
+ transport_network,
128
+ origins=self.origins,
129
+ destinations=self.destinations,
130
+ max_time=self.isochrones.max(),
131
+ **kwargs,
132
+ )
133
+
134
+ data = self._compute_isochrones_from_travel_times(travel_times)
135
+
136
+ with warnings.catch_warnings():
137
+ warnings.filterwarnings(
138
+ "ignore",
139
+ message=(
140
+ "You are adding a column named 'geometry' to a GeoDataFrame "
141
+ "constructed without an active geometry column"
142
+ ),
143
+ category=FutureWarning,
144
+ )
145
+ for column in data.columns:
146
+ self[column] = data[column]
147
+ self.set_geometry("geometry")
148
+
149
+ del self.transport_network
150
+
151
+ def _compute_isochrones_from_travel_times(self, travel_times):
152
+ travel_times = travel_times.dropna().groupby("to_id").min().reset_index()
153
+
154
+ if self.request.percentiles == [50]:
155
+ travel_time_column = "travel_time"
156
+ else:
157
+ travel_time_column = f"travel_time_p{self.request.percentiles[0]:d}"
158
+
159
+ isochrones = {
160
+ "travel_time": [],
161
+ "geometry": [],
162
+ }
163
+
164
+ for isochrone in self.isochrones:
165
+ reached_nodes = (
166
+ self.destinations.set_index("id")
167
+ .join(
168
+ travel_times[
169
+ travel_times[travel_time_column]
170
+ <= (isochrone.total_seconds() / 60)
171
+ ].set_index("to_id"),
172
+ how="inner",
173
+ )
174
+ .reset_index()
175
+ )
176
+
177
+ # isochrone polygons might be disjoint (e.g., around metro stops)
178
+ if not reached_nodes.empty:
179
+ reached_nodes = SpatiallyClusteredGeoDataFrame(
180
+ reached_nodes, eps=(2.0 * self.point_grid_resolution)
181
+ ).to_crs(self.EQUIDISTANT_CRS)
182
+ isochrone_polygons = pandas.concat(
183
+ [
184
+ (
185
+ reached_nodes[reached_nodes["cluster"] != -1]
186
+ .dissolve(by="cluster")
187
+ .concave_hull(ratio=CONCAVE_HULL_RATIO)
188
+ .buffer(VERY_SMALL_BUFFER_SIZE)
189
+ ),
190
+ (
191
+ reached_nodes[reached_nodes["cluster"] == -1].buffer(
192
+ VERY_SMALL_BUFFER_SIZE
193
+ )
194
+ ),
195
+ ]
196
+ ).union_all()
197
+
198
+ isochrones["travel_time"].append(isochrone)
199
+ isochrones["geometry"].append(isochrone_polygons)
200
+
201
+ isochrones = geopandas.GeoDataFrame(
202
+ isochrones, geometry="geometry", crs=self.EQUIDISTANT_CRS
203
+ )
204
+
205
+ # clip smaller isochrones by larger isochrones
206
+ # (concave_hull’s ratio parameter depends on input shapes and does not
207
+ # produce the same results, e.g., around bridges or at the coast line)
208
+ for row in range(len(isochrones) - 2, 0, -1):
209
+ isochrones.loc[row, "geometry"] = shapely.intersection(
210
+ isochrones.loc[row, "geometry"], isochrones.loc[row + 1, "geometry"]
211
+ )
212
+
213
+ isochrones["geometry"] = (
214
+ isochrones["geometry"]
215
+ .buffer(CONCAVE_HULL_BUFFER_SIZE)
216
+ .boundary.apply(
217
+ lambda geometry: (
218
+ geometry
219
+ if isinstance(geometry, shapely.MultiLineString)
220
+ else shapely.MultiLineString([geometry])
221
+ )
222
+ )
223
+ .apply(
224
+ lambda multilinestring: (
225
+ shapely.MultiLineString(
226
+ [
227
+ simplification.cutil.simplify_coords_vwp(
228
+ linestring.coords,
229
+ self.point_grid_resolution * 5.0,
230
+ )
231
+ for linestring in multilinestring.geoms
232
+ ]
233
+ )
234
+ )
235
+ )
236
+ .to_crs(R5_CRS)
237
+ )
238
+
239
+ return isochrones
240
+
241
+ @property
242
+ def destinations(self):
243
+ """A regular grid of points covering the range of the chosen transport mode."""
244
+ try:
245
+ return self._destinations
246
+ except AttributeError:
247
+ destinations = self._regular_point_grid
248
+ destinations["geometry"] = self.transport_network.snap_to_network(
249
+ destinations["geometry"]
250
+ )
251
+ destinations = destinations[destinations["geometry"] != EMPTY_POINT]
252
+ destinations["geometry"] = destinations["geometry"].normalize()
253
+ destinations = destinations.drop_duplicates()
254
+
255
+ # with snapping, sometimes we end up with clumps of points
256
+ # below, we try to form clusters, from all clusters we retain
257
+ # one geometry, only
258
+ destinations = SpatiallyClusteredGeoDataFrame(
259
+ destinations, eps=(0.5 * self.point_grid_resolution)
260
+ )
261
+ destinations = pandas.concat(
262
+ [
263
+ (
264
+ destinations[destinations["cluster"] != -1]
265
+ .groupby("cluster")
266
+ .first()
267
+ .set_crs(R5_CRS)
268
+ ),
269
+ destinations[destinations["cluster"] == -1],
270
+ ]
271
+ )[["id", "geometry"]].copy()
272
+
273
+ if self.point_grid_sample_ratio < 1.0:
274
+ destinations = destinations.sample(frac=self.point_grid_sample_ratio)
275
+
276
+ self._destinations = destinations
277
+
278
+ return destinations
279
+
280
+ @destinations.setter
281
+ def destinations(self, destinations):
282
+ # https://bugs.python.org/issue14965
283
+ super(self.__class__, self.__class__).destinations.__set__(self, destinations)
284
+
285
+ @property
286
+ def isochrones(self):
287
+ """
288
+ Compute isochrones for these travel times.
289
+
290
+ pandas.TimedeltaIndex | collections.abc.Iterable[int]
291
+ An iterable of integers is interpreted as minutes.
292
+ """
293
+ try:
294
+ return self._isochrones
295
+ except AttributeError:
296
+ raise
297
+
298
+ @isochrones.setter
299
+ def isochrones(self, isochrones):
300
+ if not isinstance(isochrones, pandas.TimedeltaIndex):
301
+ isochrones = pandas.to_timedelta(isochrones, unit="minutes")
302
+ try:
303
+ # do not compute for 0 travel time
304
+ isochrones = isochrones.drop(datetime.timedelta(0))
305
+ except KeyError:
306
+ pass
307
+ self._isochrones = isochrones
308
+
309
+ @property
310
+ def _regular_point_grid(self):
311
+ extent = shapely.ops.transform(
312
+ pyproj.Transformer.from_crs(
313
+ R5_CRS,
314
+ self.EQUIDISTANT_CRS,
315
+ always_xy=True,
316
+ ).transform,
317
+ self.transport_network.extent,
318
+ )
319
+
320
+ grid = geohexgrid.make_grid_from_bounds(
321
+ *extent.bounds,
322
+ self.point_grid_resolution,
323
+ crs=self.EQUIDISTANT_CRS,
324
+ )
325
+ grid["geometry"] = grid["geometry"].centroid
326
+ grid["id"] = grid.index
327
+ grid = grid[["id", "geometry"]].to_crs(R5_CRS)
328
+
329
+ # for walking and cycling, we can clip the extent to an area reachable
330
+ # by the (well-defined) travel speeds:
331
+ if set(self.request.transport_modes) <= set(
332
+ (TransportMode.WALK, TransportMode.BICYCLE)
333
+ ):
334
+ if TransportMode.WALK in self.request.transport_modes:
335
+ speed = self.request.speed_walking
336
+ if TransportMode.BICYCLE in self.request.transport_modes:
337
+ speed = self.request.speed_cycling
338
+
339
+ speed = speed * (1000.0 / 3600.0) * 1.1 # km/h -> m/s, plus a bit of buffer
340
+
341
+ grid = grid.clip(
342
+ (
343
+ pandas.concat([self.origins] * 2) # workaround until
344
+ # https://github.com/pyproj4/pyproj/issues/1309 is fixed
345
+ .to_crs(self.EQUIDISTANT_CRS)
346
+ .buffer(speed * max(self.isochrones).total_seconds())
347
+ .to_crs(R5_CRS)
348
+ )
349
+ )
350
+
351
+ return grid.copy()
r5py/r5/regional_task.py CHANGED
@@ -53,11 +53,11 @@ class RegionalTask:
53
53
  A RegionalTask wraps a `com.conveyal.r5.analyst.cluster.RegionalTask`,
54
54
  which is used to specify the details of a requested computation.
55
55
  RegionalTasks underlie virtually all major computations carried out,
56
- such as, e.g., `TravelTimeMatrixComputer` or `AccessibilityEstimator`.
56
+ such as, e.g., `TravelTimeMatrix` or `AccessibilityEstimator`.
57
57
 
58
58
  In **r5py**, there is usually no need to explicitely create a
59
59
  `RegionalTask`. Rather, the constructors to the computation classes
60
- (`TravelTimeMatrixComputer`, `AccessibilityEstimator`, ...) accept the
60
+ (`TravelTimeMatrix`, `AccessibilityEstimator`, ...) accept the
61
61
  arguments, and pass them through to an internally handled
62
62
  `RegionalTask`.
63
63
 
r5py/r5/street_layer.py CHANGED
@@ -22,6 +22,9 @@ __all__ = ["StreetLayer"]
22
22
  start_jvm()
23
23
 
24
24
 
25
+ EMPTY_POINT = shapely.Point()
26
+
27
+
25
28
  class StreetLayer:
26
29
  """Wrap a com.conveyal.r5.streets.StreetLayer."""
27
30
 
@@ -72,13 +75,14 @@ class StreetLayer:
72
75
  Closest location on the street network or `POINT EMPTY` if no
73
76
  such location could be found within `radius`
74
77
  """
75
- if split := self._street_layer.findSplit(point.y, point.x, radius, street_mode):
78
+ try:
79
+ split = self._street_layer.findSplit(point.y, point.x, radius, street_mode)
76
80
  return shapely.Point(
77
81
  split.fixedLon / com.conveyal.r5.streets.VertexStore.FIXED_FACTOR,
78
82
  split.fixedLat / com.conveyal.r5.streets.VertexStore.FIXED_FACTOR,
79
83
  )
80
- else:
81
- return shapely.Point()
84
+ except (AttributeError, TypeError):
85
+ return EMPTY_POINT
82
86
 
83
87
 
84
88
  @jpype._jcustomizer.JConversion(