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.

@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """A less complex representation of com.conveyal.r5.api.util.StreetSegment."""
4
+
5
+
6
+ import datetime
7
+
8
+ import shapely
9
+
10
+
11
+ __all__ = ["StreetSegment"]
12
+
13
+
14
+ class StreetSegment:
15
+ """A less complex representation of com.conveyal.r5.api.util.StreetSegment."""
16
+
17
+ distance = 0
18
+ duration = datetime.timedelta()
19
+ geometry = shapely.LineString()
20
+
21
+ def __init__(self, street_path):
22
+ """
23
+ Initialise a less complex representation of com.conveyal.r5.api.util.StreetSegment.
24
+
25
+ Arguments
26
+ ---------
27
+ street_path : com.conveyal.r5.profile.StreetPath
28
+ StreetPath, obtained, e.g., from StreetRouter state
29
+ """
30
+ self.distance = street_path.getDistance()
31
+ self.duration = street_path.getDuration()
32
+ self.geometry = shapely.line_merge(
33
+ shapely.MultiLineString(
34
+ [
35
+ shapely.from_wkt(
36
+ str(street_path.getEdge(edge).getGeometry().toText())
37
+ )
38
+ for edge in street_path.getEdges()
39
+ ]
40
+ )
41
+ )
r5py/r5/transit_layer.py CHANGED
@@ -98,6 +98,10 @@ class TransitLayer:
98
98
  """Return a list of GTFS trip patterns."""
99
99
  return list(self._transit_layer.tripPatterns)
100
100
 
101
+ def get_stop_id_from_index(self, stop_index):
102
+ """Get the GTFS stop id for the `stop_index`-th stop of this transit layer."""
103
+ return self._transit_layer.stopIdForIndex[stop_index]
104
+
101
105
 
102
106
  @jpype._jcustomizer.JConversion(
103
107
  "com.conveyal.r5.transit.TransitLayer", exact=TransitLayer
@@ -5,24 +5,22 @@
5
5
 
6
6
 
7
7
  import functools
8
+ import hashlib
8
9
  import pathlib
9
- import random
10
- import shutil
11
- import time
12
10
  import warnings
13
11
 
14
- import filelock
15
12
  import jpype
16
13
  import jpype.types
17
14
 
18
15
  from .street_layer import StreetLayer
19
16
  from .transit_layer import TransitLayer
20
17
  from .transport_mode import TransportMode
21
- from ..util import Config, contains_gtfs_data, start_jvm
18
+ from ..util import Config, contains_gtfs_data, FileDigest, start_jvm, WorkingCopy
22
19
 
23
20
  import com.conveyal.gtfs
24
21
  import com.conveyal.osmlib
25
22
  import com.conveyal.r5
23
+ import java.io
26
24
 
27
25
 
28
26
  __all__ = ["TransportNetwork"]
@@ -48,114 +46,63 @@ class TransportNetwork:
48
46
  gtfs : str | pathlib.Path | list[str] | list[pathlib.Path]
49
47
  path(s) to public transport schedule information in GTFS format
50
48
  """
51
- osm_pbf = self._working_copy(pathlib.Path(osm_pbf)).absolute()
49
+ osm_pbf = WorkingCopy(osm_pbf)
52
50
  if isinstance(gtfs, (str, pathlib.Path)):
53
51
  gtfs = [gtfs]
54
- gtfs = [str(self._working_copy(path).absolute()) for path in gtfs]
52
+ gtfs = [WorkingCopy(path) for path in gtfs]
55
53
 
56
- transport_network = com.conveyal.r5.transit.TransportNetwork()
57
- transport_network.scenarioId = PACKAGE
58
-
59
- osm_mapdb = pathlib.Path(f"{osm_pbf}.mapdb")
60
- osm_file = com.conveyal.osmlib.OSM(f"{osm_mapdb}")
61
- osm_file.intersectionDetection = True
62
- osm_file.readFromFile(f"{osm_pbf}")
63
-
64
- self.osm_file = osm_file # keep the mapdb open, close in destructor
65
-
66
- transport_network.streetLayer = com.conveyal.r5.streets.StreetLayer()
67
- transport_network.streetLayer.loadFromOsm(osm_file)
68
- transport_network.streetLayer.parentNetwork = transport_network
69
- transport_network.streetLayer.indexStreets()
70
-
71
- transport_network.transitLayer = com.conveyal.r5.transit.TransitLayer()
72
- for gtfs_file in gtfs:
73
- gtfs_feed = com.conveyal.gtfs.GTFSFeed.readOnlyTempFileFromGtfs(gtfs_file)
74
- transport_network.transitLayer.loadFromGtfs(gtfs_feed)
75
- gtfs_feed.close()
76
- transport_network.transitLayer.parentNetwork = transport_network
77
-
78
- transport_network.streetLayer.associateStops(transport_network.transitLayer)
79
- transport_network.streetLayer.buildEdgeLists()
80
-
81
- transport_network.transitLayer.rebuildTransientIndexes()
54
+ # a hash representing all input files
55
+ digest = hashlib.sha256(
56
+ "".join([FileDigest(osm_pbf)] + [FileDigest(path) for path in gtfs]).encode(
57
+ "utf-8"
58
+ )
59
+ ).hexdigest()
82
60
 
83
- transfer_finder = com.conveyal.r5.transit.TransferFinder(transport_network)
84
- transfer_finder.findTransfers()
85
- transfer_finder.findParkRideTransfer()
61
+ try:
62
+ transport_network = self._load_pickled_transport_network(
63
+ Config().CACHE_DIR / f"{digest}.transport_network"
64
+ )
65
+ except FileNotFoundError:
66
+ transport_network = com.conveyal.r5.transit.TransportNetwork()
67
+ transport_network.scenarioId = PACKAGE
68
+
69
+ osm_mapdb = Config().CACHE_DIR / f"{digest}.mapdb"
70
+ osm_file = com.conveyal.osmlib.OSM(f"{osm_mapdb}")
71
+ osm_file.intersectionDetection = True
72
+ osm_file.readFromFile(f"{osm_pbf}")
73
+
74
+ transport_network.streetLayer = com.conveyal.r5.streets.StreetLayer()
75
+ transport_network.streetLayer.parentNetwork = transport_network
76
+ transport_network.streetLayer.loadFromOsm(osm_file)
77
+ transport_network.streetLayer.indexStreets()
78
+
79
+ transport_network.transitLayer = com.conveyal.r5.transit.TransitLayer()
80
+ transport_network.transitLayer.parentNetwork = transport_network
81
+ for gtfs_file in gtfs:
82
+ gtfs_feed = com.conveyal.gtfs.GTFSFeed.readOnlyTempFileFromGtfs(
83
+ f"{gtfs_file}"
84
+ )
85
+ transport_network.transitLayer.loadFromGtfs(gtfs_feed)
86
+ gtfs_feed.close()
86
87
 
87
- transport_network.transitLayer.buildDistanceTables(None)
88
+ transport_network.streetLayer.associateStops(transport_network.transitLayer)
89
+ transport_network.streetLayer.buildEdgeLists()
88
90
 
89
- self._transport_network = transport_network
91
+ transport_network.transitLayer.rebuildTransientIndexes()
90
92
 
91
- def __del__(self):
92
- """Delete all temporary files upon destruction."""
93
- MAX_TRIES = 10
93
+ transfer_finder = com.conveyal.r5.transit.TransferFinder(transport_network)
94
+ transfer_finder.findTransfers()
95
+ transfer_finder.findParkRideTransfer()
94
96
 
95
- # first, close the open osm_file,
96
- # delete Java objects, and
97
- # trigger Java garbage collection
98
- try:
99
- self.osm_file.close()
100
- except jpype.JVMNotRunning:
101
- # JVM was stopped already, file should be closed
102
- pass
103
- try:
104
- del self.street_layer
105
- except AttributeError: # might not have been accessed a single time
106
- pass
107
- try:
108
- del self.transit_layer
109
- except AttributeError:
110
- pass
111
- try:
112
- del self._transport_network
113
- except AttributeError:
114
- pass
97
+ transport_network.transitLayer.buildDistanceTables(None)
115
98
 
116
- time.sleep(1.0)
117
- try:
118
- jpype.java.lang.System.gc()
119
- except jpype.JVMNotRunning:
120
- pass
99
+ osm_file.close() # not needed after here?
121
100
 
122
- # then, try to delete all files in cache directory
123
- try:
124
- temporary_files = [child for child in self._cache_directory.iterdir()]
125
- except FileNotFoundError: # deleted in the meantime/race condition
126
- temporary_files = []
127
-
128
- for _ in range(MAX_TRIES):
129
- for temporary_file in temporary_files:
130
- try:
131
- temporary_file.unlink()
132
- temporary_files.remove(temporary_file)
133
- except (FileNotFoundError, IOError, OSError):
134
- print(
135
- f"could not delete {temporary_file}, keeping in {temporary_files}"
136
- )
137
- pass
138
-
139
- if not temporary_files: # empty
140
- break
141
-
142
- # there are still files open, let’s wait a moment and try again
143
- time.sleep(0.1)
144
- else:
145
- remaining_files = ", ".join(
146
- [f"{temporary_file}" for temporary_file in temporary_files]
147
- )
148
- warnings.warn(
149
- f"Failed to clean cache directory ‘{self._cache_directory}’. "
150
- f"Remaining file(s): {remaining_files}",
151
- RuntimeWarning,
101
+ self._save_pickled_transport_network(
102
+ transport_network, Config().CACHE_DIR / f"{digest}.transport_network"
152
103
  )
153
104
 
154
- # finally, try to delete the cache directory itself
155
- try:
156
- self._cache_directory.rmdir()
157
- except OSError: # not empty
158
- pass # the JVM destructor is going to take care of this
105
+ self._transport_network = transport_network
159
106
 
160
107
  @classmethod
161
108
  def from_directory(cls, path):
@@ -219,56 +166,22 @@ class TransportNetwork:
219
166
  # then find the smaller extent of the two (or the larger one?)
220
167
  return self.street_layer.extent
221
168
 
222
- @functools.cached_property
223
- def _cache_directory(self):
224
- cache_dir = (
225
- pathlib.Path(Config().TEMP_DIR)
226
- / f"{self.__class__.__name__:s}_{id(self):x}_{random.randrange(16**5):07x}"
227
- )
228
- cache_dir.mkdir(exist_ok=True)
229
- return cache_dir
230
-
231
- def _working_copy(self, input_file):
232
- """Create a copy or link of an input file in a cache directory.
233
-
234
- This method exists because R5 creates temporary files in the
235
- directory of input files. This can not only be annoying clutter,
236
- but also create problems of concurrency, performance, etc., for
237
- instance, when the data comes from a shared network drive or a
238
- read-only file system.
239
-
240
- Arguments
241
- ---------
242
- input_file : str or pathlib.Path
243
- The file to create a copy or link of in a cache directory
244
-
245
- Returns
246
- -------
247
- pathlib.Path
248
- The path to the copy or link created
249
- """
250
- # try to first create a symbolic link, if that fails (e.g., on Windows),
251
- # copy the file to a cache directory
252
- input_file = pathlib.Path(input_file).absolute()
253
- destination_file = pathlib.Path(
254
- self._cache_directory / input_file.name
255
- ).absolute()
256
-
257
- with filelock.FileLock(
258
- destination_file.parent / f"{destination_file.name}.lock"
259
- ):
260
- if not destination_file.exists():
261
- try:
262
- destination_file.symlink_to(input_file)
263
- except OSError:
264
- shutil.copyfile(str(input_file), str(destination_file))
265
- return destination_file
266
-
267
169
  @property
268
170
  def linkage_cache(self):
269
171
  """Expose the `TransportNetwork`’s `linkageCache` to Python."""
270
172
  return self._transport_network.linkageCache
271
173
 
174
+ def _load_pickled_transport_network(self, path):
175
+ try:
176
+ input_file = java.io.File(f"{path}")
177
+ return com.conveyal.r5.kryo.KryoNetworkSerializer.read(input_file)
178
+ except java.io.FileNotFoundException:
179
+ raise FileNotFoundError
180
+
181
+ def _save_pickled_transport_network(self, transport_network, path):
182
+ output_file = java.io.File(f"{path}")
183
+ com.conveyal.r5.kryo.KryoNetworkSerializer.write(transport_network, output_file)
184
+
272
185
  def snap_to_network(
273
186
  self,
274
187
  points,
@@ -4,24 +4,83 @@
4
4
 
5
5
  import copy
6
6
 
7
+ try:
8
+ from warnings import deprecated
9
+ except ImportError: # Python<=3.12
10
+ from typing_extensions import deprecated
11
+
7
12
  import pandas
8
13
 
9
- from .base_travel_time_matrix_computer import BaseTravelTimeMatrixComputer
14
+ from .base_travel_time_matrix import BaseTravelTimeMatrix
10
15
  from ..util import start_jvm
11
16
 
12
17
  import com.conveyal.r5
13
18
 
14
19
 
15
- __all__ = ["TravelTimeMatrixComputer"]
20
+ __all__ = ["TravelTimeMatrix", "TravelTimeMatrixComputer"]
16
21
 
17
22
 
18
23
  start_jvm()
19
24
 
20
25
 
21
- class TravelTimeMatrixComputer(BaseTravelTimeMatrixComputer):
26
+ class TravelTimeMatrix(BaseTravelTimeMatrix):
22
27
  """Compute travel times between many origins and destinations."""
23
28
 
24
- def compute_travel_times(self):
29
+ def __init__(
30
+ self,
31
+ transport_network,
32
+ origins=None,
33
+ destinations=None,
34
+ snap_to_network=False,
35
+ **kwargs,
36
+ ):
37
+ """
38
+ Compute travel times between many origins and destinations.
39
+
40
+ ``r5py.TravelTimeMatrix`` are child classes of ``pandas.DataFrame`` and
41
+ support all of their methods and properties,
42
+ see https://pandas.pydata.org/docs/
43
+
44
+ Arguments
45
+ ---------
46
+ transport_network : r5py.TransportNetwork | tuple(str, list(str), dict)
47
+ The transport network to route on. This can either be a readily
48
+ initialised r5py.TransportNetwork or a tuple of the parameters
49
+ passed to ``TransportNetwork.__init__()``: the path to an OpenStreetMap
50
+ extract in PBF format, a list of zero of more paths to GTFS transport
51
+ schedule files, and a dict with ``build_config`` options.
52
+ origins : geopandas.GeoDataFrame
53
+ Places to find a route _from_
54
+ Has to have a point geometry, and at least an `id` column
55
+ destinations : geopandas.GeoDataFrame (optional)
56
+ Places to find a route _to_
57
+ Has to have a point geometry, and at least an `id` column
58
+ If omitted, use same data set as for origins
59
+ snap_to_network : bool or int, default False
60
+ Should origin an destination points be snapped to the street network
61
+ before routing? If `True`, the default search radius (defined in
62
+ `com.conveyal.r5.streets.StreetLayer.LINK_RADIUS_METERS`) is used,
63
+ if `int`, use `snap_to_network` meters as the search radius.
64
+ **kwargs : mixed
65
+ Any arguments than can be passed to r5py.RegionalTask:
66
+ ``departure``, ``departure_time_window``, ``percentiles``, ``transport_modes``,
67
+ ``access_modes``, ``egress_modes``, ``max_time``, ``max_time_walking``,
68
+ ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
69
+ ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
70
+ """
71
+ super().__init__(
72
+ transport_network,
73
+ origins,
74
+ destinations,
75
+ snap_to_network,
76
+ **kwargs,
77
+ )
78
+ data = self._compute()
79
+ for column in data.columns:
80
+ self[column] = data[column]
81
+ del self.transport_network
82
+
83
+ def _compute(self):
25
84
  """
26
85
  Compute travel times from all origins to all destinations.
27
86
 
@@ -76,17 +135,16 @@ class TravelTimeMatrixComputer(BaseTravelTimeMatrixComputer):
76
135
  travel time.
77
136
  """
78
137
  # First, create an empty DataFrame (this forces column types)
79
- travel_time_columns = {
80
- "from_id": pandas.Series(dtype=str),
81
- "to_id": pandas.Series(dtype=str),
82
- }
83
- travel_time_columns.update(
138
+ od_matrix = pandas.DataFrame(
84
139
  {
140
+ "from_id": pandas.Series(dtype=str),
141
+ "to_id": pandas.Series(dtype=str),
142
+ }
143
+ | {
85
144
  f"travel_time_p{percentile:d}": pandas.Series(dtype=float)
86
145
  for percentile in self.request.percentiles
87
146
  }
88
147
  )
89
- od_matrix = pandas.DataFrame(travel_time_columns)
90
148
 
91
149
  # first assign columns with correct length (`to_id`),
92
150
  # only then fill `from_id` (it’s a scalar)
@@ -121,3 +179,31 @@ class TravelTimeMatrixComputer(BaseTravelTimeMatrixComputer):
121
179
  od_matrix = self._parse_results(from_id, results)
122
180
 
123
181
  return od_matrix
182
+
183
+
184
+ @deprecated(
185
+ "Use `TravelTimeMatrix` instead, `TravelTimeMatrixComputer will be deprecated in a future release."
186
+ )
187
+ class TravelTimeMatrixComputer:
188
+ """Compute travel times between many origins and destinations."""
189
+
190
+ def __init__(self, *args, **kwargs):
191
+ """Compute travel times between many origins and destinations."""
192
+ self._ttm = TravelTimeMatrix(*args, **kwargs)
193
+
194
+ def compute_travel_times(self):
195
+ """
196
+ Compute travel times from all origins to all destinations.
197
+
198
+ Returns
199
+ -------
200
+ pandas.DataFrame
201
+ A data frame containing the columns ``from_id``, ``to_id``, and
202
+ ``travel_time``, where ``travel_time`` is the median calculated
203
+ travel time between ``from_id`` and ``to_id`` or ``numpy.nan``
204
+ if no connection with the given parameters was found.
205
+ If non-default ``percentiles`` were requested: one or more columns
206
+ ``travel_time_p{:02d}`` representing the particular percentile of
207
+ travel time.
208
+ """
209
+ return self._ttm
r5py/r5/trip.py CHANGED
@@ -54,8 +54,9 @@ class Trip:
54
54
  Returns
55
55
  =======
56
56
  list : detailed information about this trip and its legs (segments):
57
- ``segment``, ``transport_mode``, ``departure_time``, ``distance``,
58
- ``travel_time``, ``wait_time``, ``route``, ``geometry``
57
+ ``segment``, ``transport_mode``, ``departure_time``, ``distance``,
58
+ ``travel_time``, ``wait_time``, ``feed``, ``agency_id``, ``route_id``,
59
+ ``start_stop_id``, ``end_stop_id``, ``geometry``
59
60
  """
60
61
  return [[segment] + leg.as_table_row() for segment, leg in enumerate(self.legs)]
61
62
 
@@ -76,9 +77,9 @@ class Trip:
76
77
  )
77
78
 
78
79
  @property
79
- def routes(self):
80
+ def route_ids(self):
80
81
  """The public transport route(s) used on this trip."""
81
- return [leg.route for leg in self.legs]
82
+ return [leg.route_id for leg in self.legs]
82
83
 
83
84
  @property
84
85
  def transport_modes(self):
r5py/r5/trip_leg.py CHANGED
@@ -26,7 +26,11 @@ class TripLeg:
26
26
  "distance",
27
27
  "travel_time",
28
28
  "wait_time",
29
- "route",
29
+ "feed",
30
+ "agency_id",
31
+ "route_id",
32
+ "start_stop_id",
33
+ "end_stop_id",
30
34
  "geometry",
31
35
  ]
32
36
 
@@ -37,28 +41,41 @@ class TripLeg:
37
41
  distance=None,
38
42
  travel_time=datetime.timedelta(seconds=0),
39
43
  wait_time=datetime.timedelta(seconds=0),
40
- route=None,
44
+ feed=None,
45
+ agency_id=None,
46
+ route_id=None,
47
+ start_stop_id=None,
48
+ end_stop_id=None,
41
49
  geometry=shapely.LineString(),
42
50
  ):
43
51
  """
44
52
  Represent one leg of a trip.
45
53
 
46
- This is a base class, use one the specific classes,
47
- e.g., TransitLeg, or DirectLeg
54
+ This is a base class, use one of the more specific classes, e.g.,
55
+ TransitLeg, or DirectLeg
48
56
 
49
57
  Arguments
50
58
  =========
51
59
  transport_mode : r5py.TransportMode
52
60
  mode of transport this trip leg was travelled
53
- departure_time : datetime.datetime,
61
+ departure_time : datetime.datetime
62
+ departure time of this trip leg
54
63
  distance : float
55
64
  distance covered by this trip leg, in metres
56
65
  travel_time : datetime.timedelta
57
66
  time spent travelling on this trip leg
58
67
  wait_time : datetime.timedelta
59
68
  time spent waiting for a connection on this trip leg
60
- route : str
61
- public transport route used for this trip leg
69
+ feed : str
70
+ the GTFS feed identifier used for this trip leg
71
+ agency_id : str
72
+ the GTFS id the agency used for this trip leg
73
+ route_id : str
74
+ the GTFS id of the public transport route used for this trip leg
75
+ start_stop_id : str
76
+ the GTFS stop_id of the boarding stop used for this trip leg
77
+ end_stop_id : str
78
+ the GTFS stop_id of the aligning stop used for this trip leg
62
79
  geometry : shapely.LineString
63
80
  spatial representation of this trip leg
64
81
  """
@@ -67,7 +84,11 @@ class TripLeg:
67
84
  self.distance = distance
68
85
  self.travel_time = travel_time
69
86
  self.wait_time = wait_time
70
- self.route = route
87
+ self.feed = feed
88
+ self.agency_id = agency_id
89
+ self.route_id = route_id
90
+ self.start_stop_id = start_stop_id
91
+ self.end_stop_id = end_stop_id
71
92
  self.geometry = geometry
72
93
 
73
94
  def __add__(self, other):
@@ -177,7 +198,8 @@ class TripLeg:
177
198
  Returns
178
199
  =======
179
200
  list : detailed information about this trip leg: ``transport_mode``,
180
- ``departure_time``, ``distance``, ``travel_time``, ``wait_time``,
181
- ``route``, ``geometry``
201
+ ``departure_time``, ``distance``, ``travel_time``, ``wait_time``,
202
+ ``feed``, ``agency_id`` ``route_id``, ``start_stop_id``,
203
+ ``end_stop_id``, ``geometry``
182
204
  """
183
205
  return [getattr(self, column) for column in self.COLUMNS]
r5py/r5/trip_planner.py CHANGED
@@ -17,6 +17,7 @@ import shapely
17
17
  from .access_leg import AccessLeg
18
18
  from .direct_leg import DirectLeg
19
19
  from .egress_leg import EgressLeg
20
+ from .street_segment import StreetSegment
20
21
  from .transfer_leg import TransferLeg
21
22
  from .transit_leg import TransitLeg
22
23
  from .transport_mode import TransportMode
@@ -64,7 +65,9 @@ class TripPlanner:
64
65
 
65
66
  EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(self.transport_network.extent)
66
67
  self._crs_transformer_function = pyproj.Transformer.from_crs(
67
- R5_CRS, EQUIDISTANT_CRS
68
+ R5_CRS,
69
+ EQUIDISTANT_CRS,
70
+ always_xy=True,
68
71
  ).transform
69
72
 
70
73
  @property
@@ -165,17 +168,13 @@ class TripPlanner:
165
168
  return direct_paths
166
169
 
167
170
  def _street_segment_from_router_state(self, router_state, transport_mode):
168
- """Retrieve a com.conveyal.r5.street.StreetSegment for a route."""
171
+ """Retrieve a StreetSegment for a route."""
169
172
  street_path = com.conveyal.r5.profile.StreetPath(
170
173
  router_state,
171
174
  self.transport_network,
172
175
  False,
173
176
  )
174
- street_segment = com.conveyal.r5.api.util.StreetSegment(
175
- street_path,
176
- transport_mode,
177
- self.transport_network.street_layer,
178
- )
177
+ street_segment = StreetSegment(street_path)
179
178
  return street_segment
180
179
 
181
180
  @functools.cached_property
@@ -216,7 +215,6 @@ class TripPlanner:
216
215
  distance=0.0,
217
216
  travel_time=ZERO_SECONDS,
218
217
  wait_time=ZERO_SECONDS,
219
- route=None,
220
218
  geometry=shapely.LineString(((lon, lat), (lon, lat))),
221
219
  )
222
220
  ]
@@ -327,6 +325,17 @@ class TripPlanner:
327
325
 
328
326
  else: # TransitLeg
329
327
  pattern = transit_layer.trip_patterns[state.pattern]
328
+
329
+ # Use the indices to look up the stop ids, which are scoped by the GTFS feed supplied
330
+ start_stop_id = transit_layer.get_stop_id_from_index(
331
+ state.back.stop
332
+ ).split(":")[1]
333
+ end_stop = transit_layer.get_stop_id_from_index(
334
+ state.stop
335
+ )
336
+ end_stop_id = end_stop.split(":")[1]
337
+ feed = end_stop.split(":")[0]
338
+
330
339
  route = transit_layer.routes[pattern.routeIndex]
331
340
  transport_mode = TransportMode(
332
341
  com.conveyal.r5.transit.TransitLayer.getTransitModes(
@@ -379,13 +388,17 @@ class TripPlanner:
379
388
  distance = None
380
389
 
381
390
  leg = TransitLeg(
382
- transport_mode,
383
- departure_time,
384
- distance,
385
- travel_time,
386
- wait_time,
387
- str(route.route_short_name),
388
- geometry,
391
+ transport_mode=transport_mode,
392
+ departure_time=departure_time,
393
+ distance=distance,
394
+ travel_time=travel_time,
395
+ wait_time=wait_time,
396
+ feed=str(feed),
397
+ agency_id=str(route.agency_id),
398
+ route_id=str(route.route_id),
399
+ start_stop_id=str(start_stop_id),
400
+ end_stop_id=str(end_stop_id),
401
+ geometry=geometry,
389
402
  )
390
403
 
391
404
  # we traverse in reverse order:
r5py/util/__init__.py CHANGED
@@ -8,18 +8,24 @@ from .camel_to_snake_case import camel_to_snake_case
8
8
  from .config import Config
9
9
  from .contains_gtfs_data import contains_gtfs_data
10
10
  from .data_validation import check_od_data_set
11
+ from .file_digest import FileDigest
11
12
  from .good_enough_equidistant_crs import GoodEnoughEquidistantCrs
12
13
  from .jvm import start_jvm
13
14
  from .parse_int_date import parse_int_date
14
15
  from .snake_to_camel_case import snake_to_camel_case
16
+ from .spatially_clustered_geodataframe import SpatiallyClusteredGeoDataFrame
17
+ from .working_copy import WorkingCopy
15
18
 
16
19
  __all__ = [
17
20
  "camel_to_snake_case",
18
21
  "check_od_data_set",
19
22
  "Config",
20
23
  "contains_gtfs_data",
24
+ "FileDigest",
21
25
  "GoodEnoughEquidistantCrs",
22
26
  "parse_int_date",
23
27
  "snake_to_camel_case",
28
+ "SpatiallyClusteredGeoDataFrame",
24
29
  "start_jvm",
30
+ "WorkingCopy",
25
31
  ]