@@ -66,9 +66,17 @@ public class PickupDelay extends Modification {
66
66
public String idAttribute = "id" ;
67
67
68
68
/**
69
- * A JSON map from polygon IDs to lists of polygon IDs. If any stop_id is specified for a polygon, service is
70
- * only allowed between the polygon and the stops (i.e. no direct trips). If no stop_ids are specified,
71
- * passengers boarding an on-demand service in a pick-up zone should be able to alight anywhere.
69
+ * A JSON map from polygon IDs to lists of polygon IDs, representing origin zones and allowable destination zones
70
+ * for direct legs. For example, "a":["b","c"] allows passengers to travel from zone a to destinations in zones
71
+ * b and c. For now, this does not enable use of stops in zones b and c for onward travel, so in most cases,
72
+ * these zones should be explicitly repeated in stopsForZone.
73
+ */
74
+ public Map <String , Set <String >> destinationsForZone ;
75
+
76
+ /**
77
+ * A JSON map from polygon IDs to lists of polygon IDs, representing origin zones and allowable boarding stops
78
+ * for access legs. For example "a":["d","e"] allows passengers to travel from zone a to transit stops in zones d
79
+ * and e.
72
80
*/
73
81
public Map <String , Set <String >> stopsForZone ;
74
82
@@ -114,58 +122,68 @@ public boolean resolve (TransportNetwork network) {
114
122
// Collect any errors from the IndexedPolygonCollection construction, so they can be seen in the UI.
115
123
errors .addAll (polygons .getErrors ());
116
124
// Handle pickup service to stop mapping if supplied in the modification JSON.
117
- if (stopsForZone == null ) {
118
- this .pickupWaitTimes = new PickupWaitTimes (polygons , null , Collections .emptySet (), this .streetMode );
125
+ if (stopsForZone == null && destinationsForZone == null ) {
126
+ this .pickupWaitTimes = new PickupWaitTimes (
127
+ polygons ,
128
+ null ,
129
+ null ,
130
+ Collections .emptySet (),
131
+ this .streetMode
132
+ );
119
133
} else {
120
- // Iterate over all zone-stop mappings and resolve them against the network.
134
+ // Iterate over all zone-zone mappings and resolve them against the network.
121
135
// Because they are used in lambda functions, these variables must be final and non-null.
122
136
// That is why there's a separate PickupWaitTimes constructor call above with a null parameter.
123
137
final Map <ModificationPolygon , TIntSet > stopNumbersForZonePolygon = new HashMap <>();
124
- final Map <ModificationPolygon , PickupWaitTimes .EgressService > egressServices = new HashMap <>();
125
- if (stopsForZone .isEmpty ()) {
126
- errors .add ("If stopsForZone is specified, it must be non-empty." );
127
- }
128
- stopsForZone .forEach ((zonePolygonId , stopPolygonIds ) -> {
129
- ModificationPolygon zonePolygon = polygons .getById (zonePolygonId );
130
- if (zonePolygon == null ) {
131
- errors .add ("Could not find zone polygon with ID: " + zonePolygonId );
132
- }
133
- TIntSet stopNumbers = stopNumbersForZonePolygon .get (zonePolygon );
134
- if (stopNumbers == null ) {
135
- stopNumbers = new TIntHashSet ();
136
- stopNumbersForZonePolygon .put (zonePolygon , stopNumbers );
137
- }
138
- for (String stopPolygonId : stopPolygonIds ) {
139
- ModificationPolygon stopPolygon = polygons .getById (stopPolygonId );
140
- if (stopPolygon == null ) {
141
- errors .add ("Could not find stop polygon with ID: " + stopPolygonId );
138
+ final Map <ModificationPolygon , Geometry > destinationsForZonePolygon = new HashMap <>();
139
+ final Map <ModificationPolygon , EgressService > egressServices = new HashMap <>();
140
+ if (destinationsForZone != null ) {
141
+ destinationsForZone .forEach ((zonePolygonId , destinationPolygonIds ) -> {
142
+ ModificationPolygon zonePolygon = tryToGetPolygon (polygons , zonePolygonId , "zone" );
143
+ for (String id : destinationPolygonIds ) {
144
+ ModificationPolygon destinationPolygon = tryToGetPolygon (polygons , id , "destination" );
145
+ destinationsForZonePolygon .put (zonePolygon , destinationPolygon .polygonal );
142
146
}
143
- TIntSet stops = network .transitLayer .findStopsInGeometry (stopPolygon .polygonal );
144
- if (stops .isEmpty ()) {
145
- errors .add ("Stop polygon did not contain any stops: " + stopPolygonId );
147
+ });
148
+ }
149
+ if (stopsForZone != null ) {
150
+ stopsForZone .forEach ((zonePolygonId , stopPolygonIds ) -> {
151
+ ModificationPolygon zonePolygon = tryToGetPolygon (polygons , zonePolygonId , "zone" );
152
+ TIntSet stopNumbers = stopNumbersForZonePolygon .get (zonePolygon );
153
+ if (stopNumbers == null ) {
154
+ stopNumbers = new TIntHashSet ();
155
+ stopNumbersForZonePolygon .put (zonePolygon , stopNumbers );
146
156
}
147
- stopNumbers .addAll (stops );
148
- // Derive egress services from this pair of polygons
149
- double egressWaitMinutes = stopPolygon .data ;
150
- if (egressWaitMinutes >= 0 ) {
151
- // This stop polygon can be used on the egress end of a trip.
152
- int egressWaitSeconds = (int ) (egressWaitMinutes * 60 );
153
- Geometry serviceArea = zonePolygon .polygonal ;
154
- PickupWaitTimes .EgressService egressService = egressServices .get (stopPolygon );
155
- if (egressService != null ) {
156
- // Merge service are with any other service polygons associated with this stop polygon.
157
- serviceArea = serviceArea .union (egressService .serviceArea );
157
+ for (String stopPolygonId : stopPolygonIds ) {
158
+ ModificationPolygon stopPolygon = tryToGetPolygon (polygons , stopPolygonId , "stop" );
159
+ TIntSet stops = network .transitLayer .findStopsInGeometry (stopPolygon .polygonal );
160
+ if (stops .isEmpty ()) {
161
+ errors .add ("Stop polygon did not contain any stops: " + stopPolygonId );
162
+ }
163
+ stopNumbers .addAll (stops );
164
+ // Derive egress services from this pair of polygons
165
+ double egressWaitMinutes = stopPolygon .data ;
166
+ if (egressWaitMinutes >= 0 ) {
167
+ // This stop polygon can be used on the egress end of a trip.
168
+ int egressWaitSeconds = (int ) (egressWaitMinutes * 60 );
169
+ Geometry serviceArea = zonePolygon .polygonal ;
170
+ EgressService egressService = egressServices .get (stopPolygon );
171
+ if (egressService != null ) {
172
+ // Merge service area with any other service polygons associated with this stop polygon.
173
+ serviceArea = serviceArea .union (egressService .serviceArea );
174
+ }
175
+ egressService = new EgressService (egressWaitSeconds , stops , serviceArea );
176
+ egressServices .put (stopPolygon , egressService );
158
177
}
159
- egressService = new PickupWaitTimes .EgressService (egressWaitSeconds , stops , serviceArea );
160
- egressServices .put (stopPolygon , egressService );
161
178
}
162
- }
163
- });
179
+
180
+ });
181
+ }
164
182
// TODO filter out polygons that aren't keys in stopsForZone using new IndexedPolygonCollection constructor
165
- // egress wait times for stop numbers
166
183
this .pickupWaitTimes = new PickupWaitTimes (
167
184
polygons ,
168
185
stopNumbersForZonePolygon ,
186
+ destinationsForZonePolygon ,
169
187
egressServices .values (),
170
188
this .streetMode
171
189
);
@@ -177,6 +195,14 @@ public boolean resolve (TransportNetwork network) {
177
195
return errors .size () > 0 ;
178
196
}
179
197
198
+ private ModificationPolygon tryToGetPolygon (IndexedPolygonCollection polygons , String id , String label ) {
199
+ ModificationPolygon polygon = polygons .getById (id );
200
+ if (polygon == null ) {
201
+ errors .add ("Could not find " + label + " polygon with ID: " + id );
202
+ }
203
+ return polygon ;
204
+ }
205
+
180
206
@ Override
181
207
public boolean apply (TransportNetwork network ) {
182
208
// network.streetLayer is already a protective copy made by method Scenario.applyToTransportNetwork.
0 commit comments