Skip to content

Commit 70979b8

Browse files
committed
Feature: allow direct on-demand service to limited zones
1 parent 860221f commit 70979b8

File tree

5 files changed

+107
-111
lines changed

5 files changed

+107
-111
lines changed

src/main/java/com/conveyal/r5/analyst/TravelTimeComputer.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,12 @@ public OneOriginResult computeTravelTimes() {
222222
);
223223

224224
if (accessService != NO_WAIT_ALL_STOPS) {
225-
LOG.info("Delaying direct travel times by {} seconds (to wait for {} pick-up).",
225+
LOG.info("Delaying on-demand service by {} seconds (to wait for {} pick-up).",
226226
accessService.waitTimeSeconds, accessMode);
227-
if (accessService.stopsReachable != null) {
227+
if (accessService.serviceArea != null) {
228+
pointSetTimes.incrementWithinAndClip(accessService.serviceArea, accessService.waitTimeSeconds);
229+
}
230+
else if (accessService.stops != null) {
228231
// Disallow direct travel to destination if pickupDelay zones are associated with stops.
229232
pointSetTimes = PointSetTimes.allUnreached(destinations);
230233
} else {

src/main/java/com/conveyal/r5/analyst/scenario/PickupDelay.java

+69-43
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,17 @@ public class PickupDelay extends Modification {
6666
public String idAttribute = "id";
6767

6868
/**
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.
7280
*/
7381
public Map<String, Set<String>> stopsForZone;
7482

@@ -114,58 +122,68 @@ public boolean resolve (TransportNetwork network) {
114122
// Collect any errors from the IndexedPolygonCollection construction, so they can be seen in the UI.
115123
errors.addAll(polygons.getErrors());
116124
// 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+
);
119133
} 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.
121135
// Because they are used in lambda functions, these variables must be final and non-null.
122136
// That is why there's a separate PickupWaitTimes constructor call above with a null parameter.
123137
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);
142146
}
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);
146156
}
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);
158177
}
159-
egressService = new PickupWaitTimes.EgressService(egressWaitSeconds, stops, serviceArea);
160-
egressServices.put(stopPolygon, egressService);
161178
}
162-
}
163-
});
179+
180+
});
181+
}
164182
// TODO filter out polygons that aren't keys in stopsForZone using new IndexedPolygonCollection constructor
165-
// egress wait times for stop numbers
166183
this.pickupWaitTimes = new PickupWaitTimes(
167184
polygons,
168185
stopNumbersForZonePolygon,
186+
destinationsForZonePolygon,
169187
egressServices.values(),
170188
this.streetMode
171189
);
@@ -177,6 +195,14 @@ public boolean resolve (TransportNetwork network) {
177195
return errors.size() > 0;
178196
}
179197

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+
180206
@Override
181207
public boolean apply (TransportNetwork network) {
182208
// network.streetLayer is already a protective copy made by method Scenario.applyToTransportNetwork.

src/main/java/com/conveyal/r5/analyst/scenario/PickupWaitTimes.java

+18-65
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,18 @@ public class PickupWaitTimes {
3535
* ends, it should contain even polygons whose wait time data is negative (indicating they can't be used on access)
3636
* because they may still be used for egress. TODO Clarify -- this implies stop zones should have delay = -1? But
3737
* that's not congruent with egressWaitMinutes >= 0 in PickupDelay.
38-
* If the Map is not present (null) then all the zones allow access to any stop in the network.
38+
* If this and destinationAreasForZonePolygon are both null, then all the zones allow access to any stop in the
39+
* network.
3940
*/
4041
private final Map<ModificationPolygon, TIntSet> stopNumbersForZonePolygon;
4142

43+
/**
44+
* Map associating each on-demand pick-up zone with specific areas to which direct service (without using a
45+
* scheduled transit mode) is provided. If this and stopNumbersForZonePolygon are both null, then all the zones
46+
* provide service to all destinations.
47+
*/
48+
private final Map<ModificationPolygon, Geometry> destinationAreasForZonePolygon;
49+
4250
private final TIntObjectMap<EgressService> egressServiceForStop;
4351

4452
// A temporary reversed Multimap should be made, maybe only when building the egress tables, mapping each stop
@@ -57,14 +65,16 @@ public class PickupWaitTimes {
5765
public PickupWaitTimes (
5866
IndexedPolygonCollection polygons,
5967
Map<ModificationPolygon, TIntSet> stopNumbersForZonePolygon,
68+
Map<ModificationPolygon, Geometry> destinationAreasForZonePolygon,
6069
Collection<EgressService> egressServices,
6170
StreetMode streetMode
6271
) {
6372
this.polygons = polygons;
6473
this.stopNumbersForZonePolygon = stopNumbersForZonePolygon;
74+
this.destinationAreasForZonePolygon = destinationAreasForZonePolygon;
6575
this.egressServiceForStop = new TIntObjectHashMap<>();
6676
for (EgressService egressService : egressServices) {
67-
egressService.egressStops.forEach(stop -> {
77+
egressService.stops.forEach(stop -> {
6878
egressServiceForStop.put(stop, egressService);
6979
return true;
7080
});
@@ -75,7 +85,8 @@ public PickupWaitTimes (
7585
/**
7686
* Given a particular departure location, get a description of the on-demand pickup service available there.
7787
* Currently this chooses just one "best" zone polygon based on location and priority values in the polygons.
78-
* @return an AccessService with the wait time to be picked up, and any restrictions on reachable stops.
88+
* @return an AccessService with the wait time to be picked up, and any restrictions on reachable stops and
89+
* service areas
7990
*/
8091
public AccessService getAccessService (double lat, double lon) {
8192
Point point = GeometryUtils.geometryFactory.createPoint(new Coordinate(lon, lat));
@@ -97,7 +108,10 @@ public AccessService getAccessService (double lat, double lon) {
97108
stopsReachable = new TIntHashSet();
98109
}
99110
}
100-
return new AccessService(waitTimeSeconds, stopsReachable);
111+
112+
Geometry directAreasReachable = destinationAreasForZonePolygon.get(polygon);
113+
114+
return new AccessService(waitTimeSeconds, stopsReachable, directAreasReachable);
101115
}
102116

103117
/**
@@ -108,67 +122,6 @@ public EgressService getEgressService (int stopNumber) {
108122
return egressServiceForStop.get(stopNumber);
109123
}
110124

111-
// TODO superclass Service contains all fields, and is the type of these two constants?
112-
113-
/** Special instance representing situations where a service is defined, but not available at this location. */
114-
public static final AccessService NO_SERVICE_HERE = new AccessService(-1, null);
115-
116-
/** Special instance representing no on-demand service defined, so we can access all stops with no wait. */
117-
public static final AccessService NO_WAIT_ALL_STOPS = new AccessService(0, null);
118-
119-
/**
120-
* This represents an on-demand service available at a particular departure location.
121-
* This is the result of evaluating the PickupWaitTimes at a particular place.
122-
* Alternatively instead of defining this data-holder class we could allow passing a travel time map into a method
123-
* on this class, and have this class transform it.
124-
*/
125-
public static class AccessService {
126-
127-
/**
128-
* The amount of time you have to wait at this location (in seconds) to be picked up on demand. Zero if you can
129-
* be picked up immediately (e.g. a taxi stand outside a station), -1 if no service is available at all.
130-
*/
131-
public final int waitTimeSeconds;
132-
133-
/**
134-
* If a limitation is placed on the transit stops one is allowed to access using this service, this is the
135-
* set of allowed stops. This is null if all stops are reachable and no filtering should happen.
136-
* If we eventually want to reflect multiple services with different waits, we could instead return a
137-
* TIntIntMap from allowed stop indexes to wait times.
138-
*/
139-
public final TIntSet stopsReachable;
140-
141-
public AccessService (int waitTimeSeconds, TIntSet stopsReachable) {
142-
this.waitTimeSeconds = waitTimeSeconds;
143-
this.stopsReachable = stopsReachable;
144-
}
145-
}
146-
147-
// TODO pull all of these classes out into an on-demand Java package
148-
// It's a bit weird that EgressServices are pre-computed and AccessService instances are built on demand.
149-
// The two classes could be almost identical with the exception of comments. One could extend the other.
150-
public static class EgressService {
151-
152-
public final int waitTimeSeconds;
153-
154-
public final TIntSet egressStops;
155-
156-
/**
157-
* The geographic area one is allowed to access using this service. If null, no geographic restriction applies.
158-
* This may be the union of several polygons that were all associated with one stop or group of stops.
159-
* In floating point WGS84 (lon, lat) coordinates. Should be a polygon or multipolygon.
160-
*/
161-
public final Geometry serviceArea;
162-
163-
public EgressService (int waitTimeSeconds, TIntSet egressStops, Geometry serviceArea) {
164-
if (waitTimeSeconds < 0) throw new AssertionError("Wait times should always be non-negative.");
165-
this.waitTimeSeconds = waitTimeSeconds;
166-
this.egressStops = egressStops;
167-
this.serviceArea = serviceArea;
168-
}
169-
170-
}
171-
172125
public int getDefaultWaitInSeconds() {
173126
return (int) (polygons.defaultData * 60);
174127
}

src/main/java/com/conveyal/r5/streets/PointSetTimes.java

+15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.conveyal.r5.streets;
22

3+
import com.conveyal.gtfs.Geometries;
34
import com.conveyal.r5.analyst.PointSet;
45
import com.conveyal.r5.profile.FastRaptorWorker;
6+
import org.locationtech.jts.geom.Coordinate;
7+
import org.locationtech.jts.geom.Geometry;
58

69
import java.util.Arrays;
710

@@ -33,6 +36,18 @@ public static PointSetTimes allUnreached (PointSet pointSet) {
3336
return new PointSetTimes(pointSet, times);
3437
}
3538

39+
public void incrementWithinAndClip(Geometry area, int seconds) {
40+
for (int i = 0; i < travelTimes.length; i++) {
41+
if (travelTimes[i] != UNREACHED &&
42+
area.contains(Geometries.geometryFactory.createPoint(new Coordinate(pointSet.getLon(i), pointSet.getLat(i)))))
43+
{
44+
travelTimes[i] += seconds;
45+
} else {
46+
travelTimes[i] = UNREACHED;
47+
}
48+
}
49+
}
50+
3651
public int size() {
3752
return travelTimes.length;
3853
}

src/main/java/com/conveyal/r5/streets/StreetLayer.java

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454

5555
import static com.conveyal.r5.analyst.scenario.ondemand.AccessService.NO_WAIT_ALL_STOPS;
5656
import static com.conveyal.r5.common.GeometryUtils.checkWgsEnvelopeSize;
57-
import static com.conveyal.r5.streets.VertexStore.fixedDegreeGeometryToFloating;
5857

5958
/**
6059
* This class stores the street network. Information about public transit is in a separate layer.

0 commit comments

Comments
 (0)