r5py 1.0.0.dev11__tar.gz → 1.0.0.dev12__tar.gz

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.

Files changed (82) hide show
  1. {r5py-1.0.0.dev11/src/r5py.egg-info → r5py-1.0.0.dev12}/PKG-INFO +5 -3
  2. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/README.md +1 -1
  3. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/pyproject.toml +4 -2
  4. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/__init__.py +3 -1
  5. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/__init__.py +2 -0
  6. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/detailed_itineraries.py +11 -2
  7. r5py-1.0.0.dev12/src/r5py/r5/isochrones.py +351 -0
  8. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/street_layer.py +7 -3
  9. r5py-1.0.0.dev12/src/r5py/r5/street_segment.py +41 -0
  10. r5py-1.0.0.dev12/src/r5py/r5/transport_network.py +236 -0
  11. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/travel_time_matrix.py +1 -0
  12. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/trip_planner.py +6 -7
  13. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/__init__.py +6 -0
  14. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/config.py +18 -2
  15. r5py-1.0.0.dev12/src/r5py/util/file_digest.py +42 -0
  16. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/good_enough_equidistant_crs.py +8 -3
  17. r5py-1.0.0.dev12/src/r5py/util/spatially_clustered_geodataframe.py +78 -0
  18. r5py-1.0.0.dev12/src/r5py/util/working_copy.py +44 -0
  19. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12/src/r5py.egg-info}/PKG-INFO +5 -3
  20. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py.egg-info/SOURCES.txt +9 -1
  21. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py.egg-info/requires.txt +3 -1
  22. r5py-1.0.0.dev12/tests/test_config.py +48 -0
  23. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_detailed_itineraries.py +138 -63
  24. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_deterministic_behaviour.py +9 -8
  25. r5py-1.0.0.dev12/tests/test_file_digest.py +65 -0
  26. r5py-1.0.0.dev12/tests/test_isochrones.py +237 -0
  27. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_transport_network.py +0 -48
  28. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_travel_time_matrix.py +2 -2
  29. r5py-1.0.0.dev12/tests/test_working_directory.py +39 -0
  30. r5py-1.0.0.dev11/src/r5py/r5/transport_network.py +0 -323
  31. r5py-1.0.0.dev11/tests/test_config.py +0 -27
  32. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/LICENSE +0 -0
  33. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/setup.cfg +0 -0
  34. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/__main__.py +0 -0
  35. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/access_leg.py +0 -0
  36. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/base_travel_time_matrix.py +0 -0
  37. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/breakdown_stat.py +0 -0
  38. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/direct_leg.py +0 -0
  39. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/egress_leg.py +0 -0
  40. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/regional_task.py +0 -0
  41. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/scenario.py +0 -0
  42. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/transfer_leg.py +0 -0
  43. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/transit_layer.py +0 -0
  44. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/transit_leg.py +0 -0
  45. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/transport_mode.py +0 -0
  46. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/trip.py +0 -0
  47. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/r5/trip_leg.py +0 -0
  48. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/camel_to_snake_case.py +0 -0
  49. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/classpath.py +0 -0
  50. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/contains_gtfs_data.py +0 -0
  51. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/data_validation.py +0 -0
  52. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/environment.py +0 -0
  53. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/exceptions.py +0 -0
  54. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/jvm.py +0 -0
  55. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/memory_footprint.py +0 -0
  56. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/parse_int_date.py +0 -0
  57. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/sample_data_set.py +0 -0
  58. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/snake_to_camel_case.py +0 -0
  59. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/validating_requests_session.py +0 -0
  60. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py/util/warnings.py +0 -0
  61. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py.egg-info/dependency_links.txt +0 -0
  62. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/src/r5py.egg-info/top_level.txt +0 -0
  63. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_breakdownstats.py +0 -0
  64. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_camel_to_snake_case.py +0 -0
  65. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_classpath.py +0 -0
  66. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_contains_gtfs_data.py +0 -0
  67. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_data_validation.py +0 -0
  68. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_good_enough_equidistant_crs.py +0 -0
  69. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_java_casting.py +0 -0
  70. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_memory_footprint.py +0 -0
  71. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_parse_int_date.py +0 -0
  72. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_regional_task.py +0 -0
  73. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_sample_data_set.py +0 -0
  74. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_snake_to_camel_case.py +0 -0
  75. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_street_layer.py +0 -0
  76. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_transit_layer.py +0 -0
  77. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_transport_mode.py +0 -0
  78. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_trip.py +0 -0
  79. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_trip_leg.py +0 -0
  80. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_trip_planner.py +0 -0
  81. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_validating_request_session.py +0 -0
  82. {r5py-1.0.0.dev11 → r5py-1.0.0.dev12}/tests/test_verbose_warnings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: r5py
3
- Version: 1.0.0.dev11
3
+ Version: 1.0.0.dev12
4
4
  Summary: Python wrapper for the R5 routing analysis engine
5
5
  Author: Christoph Fink, Willem Klumpenhouwer, Marcus Sairava, Rafael Pereira, Henrikki Tenkanen
6
6
  License: GPL-3.0-or-later or MIT
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: ConfigArgParse
20
20
  Requires-Dist: filelock
21
- Requires-Dist: fiona
21
+ Requires-Dist: geohexgrid
22
22
  Requires-Dist: geopandas
23
23
  Requires-Dist: joblib
24
24
  Requires-Dist: jpype1
@@ -27,7 +27,9 @@ Requires-Dist: pandas>=2.1.0
27
27
  Requires-Dist: psutil
28
28
  Requires-Dist: pyproj
29
29
  Requires-Dist: requests
30
+ Requires-Dist: scikit-learn
30
31
  Requires-Dist: shapely>=2.0
32
+ Requires-Dist: simplification
31
33
  Provides-Extra: docs
32
34
  Requires-Dist: contextily; extra == "docs"
33
35
  Requires-Dist: folium; extra == "docs"
@@ -154,7 +156,7 @@ your project better.
154
156
  <!-- (2) other links -->
155
157
  [conda-create-env-from-yml]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file
156
158
  [conveyal]: https://www.conveyal.com/
157
- [env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py_distro.yaml
159
+ [env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py.yaml
158
160
  [geopandas]: https://geopandas.org/
159
161
  [r5-github]: https://github.com/conveyal/r5/
160
162
  [r5r-github]: https://github.com/ipeaGIT/r5r/
@@ -96,7 +96,7 @@ your project better.
96
96
  <!-- (2) other links -->
97
97
  [conda-create-env-from-yml]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file
98
98
  [conveyal]: https://www.conveyal.com/
99
- [env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py_distro.yaml
99
+ [env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py.yaml
100
100
  [geopandas]: https://geopandas.org/
101
101
  [r5-github]: https://github.com/conveyal/r5/
102
102
  [r5r-github]: https://github.com/ipeaGIT/r5r/
@@ -18,7 +18,7 @@ authors = [
18
18
  dependencies = [
19
19
  "ConfigArgParse",
20
20
  "filelock",
21
- "fiona",
21
+ "geohexgrid",
22
22
  "geopandas",
23
23
  "joblib",
24
24
  "jpype1",
@@ -27,7 +27,9 @@ dependencies = [
27
27
  "psutil",
28
28
  "pyproj",
29
29
  "requests",
30
- "shapely>=2.0"
30
+ "scikit-learn",
31
+ "shapely>=2.0",
32
+ "simplification"
31
33
  ]
32
34
  requires-python = ">=3.9"
33
35
 
@@ -2,12 +2,13 @@
2
2
 
3
3
  """Python wrapper for the R5 routing analysis engine."""
4
4
 
5
- __version__ = "1.0.0.dev11"
5
+ __version__ = "1.0.0.dev12"
6
6
 
7
7
 
8
8
  from .r5 import (
9
9
  DetailedItineraries,
10
10
  DetailedItinerariesComputer,
11
+ Isochrones,
11
12
  RegionalTask,
12
13
  TransportMode,
13
14
  TransportNetwork,
@@ -18,6 +19,7 @@ from .r5 import (
18
19
  __all__ = [
19
20
  "DetailedItineraries",
20
21
  "DetailedItinerariesComputer",
22
+ "Isochrones",
21
23
  "RegionalTask",
22
24
  "TransportMode",
23
25
  "TransportNetwork",
@@ -7,6 +7,7 @@ from .breakdown_stat import BreakdownStat
7
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
@@ -25,6 +26,7 @@ __all__ = [
25
26
  "DetailedItinerariesComputer",
26
27
  "DirectLeg",
27
28
  "EgressLeg",
29
+ "Isochrones",
28
30
  "RegionalTask",
29
31
  "Scenario",
30
32
  "SpeedConfig",
@@ -84,7 +84,7 @@ class DetailedItineraries(BaseTravelTimeMatrix):
84
84
  ``access_modes``, ``egress_modes``, ``max_time``, ``max_time_walking``,
85
85
  ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
86
86
  ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
87
- 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
88
88
  underlying R5 engine might ignore some of them.
89
89
  """
90
90
  super().__init__(
@@ -122,11 +122,20 @@ class DetailedItineraries(BaseTravelTimeMatrix):
122
122
 
123
123
  data = self._compute()
124
124
  with warnings.catch_warnings():
125
- warnings.simplefilter("ignore", category=FutureWarning)
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
+ )
126
133
  for column in data.columns:
127
134
  self[column] = data[column]
128
135
  self.set_geometry("geometry")
129
136
 
137
+ del self.transport_network
138
+
130
139
  def _compute(self):
131
140
  """
132
141
  Compute travel times from all origins to all destinations.
@@ -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()
@@ -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(
@@ -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
+ )