Skip to content

Commit

Permalink
Support TransitRouteStops where boarding or alighting is not allowed
Browse files Browse the repository at this point in the history
In reality, there are trains that call at stops but where it might not be allowed to enter or exit such trains. Examples are night trains, where on the first few stops, only boarding is allowed, while at the last few stops only alighting is allowed. This means that it is not allowed to use such night trains to just travel between the first few stops.

This commit adds two optional attributes `allowBoarding` and `allowAlighting` to TransitRouteStops, with the default value being true for backwards compatibility and when the attribute is not specified. SwissRailRaptor respects these attributes and will not return routes where agents would enter a vehicle at a route stop where boarding is not allowed, or where agents would exit a vehicle at a route stop where alighting is not allowed.
  • Loading branch information
mrieser committed Jun 22, 2024
1 parent 5960389 commit 2c5374a
Show file tree
Hide file tree
Showing 9 changed files with 536 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ public RaptorRoute calcLeastCostRoute(double depTime, Facility fromFacility, Fac
// if it's intermodal, we still start here, as we might transfer to another close-by but non-intermodal stop.
continue;
}
if (!routeStop.routeStop.isAllowBoarding()) {
continue;
}

RRoute route = this.data.routes[routeStop.transitRouteIndex];
int depOffset = routeStop.departureOffset;

Expand Down Expand Up @@ -318,6 +322,9 @@ public List<RaptorRoute> calcRoutes(double earliestDepTime, double desiredDepTim
// this is the last stop of a route
continue;
}
if (!routeStop.routeStop.isAllowBoarding()) {
continue;
}
RRoute route = this.data.routes[routeStop.transitRouteIndex];
int depOffset = routeStop.departureOffset;
for (int depIndex = route.indexFirstDeparture; depIndex < route.indexFirstDeparture + route.countDepartures; depIndex++) {
Expand Down Expand Up @@ -502,13 +509,15 @@ public Map<Id<TransitStopFacility>, TravelInfo> calcLeastCostTree(double depTime
int[] routeStopIndices = this.data.routeStopsPerStopFacility.get(stop.stop);
for (int routeStopIndex : routeStopIndices) {
boolean useStop = true;
if (parameters.isExactDeparturesOnly()) {
RRouteStop routeStop = this.data.routeStops[routeStopIndex];
RRouteStop routeStop = this.data.routeStops[routeStopIndex];
if (!routeStop.routeStop.isAllowBoarding()) {
useStop = false;
}
if (useStop && parameters.isExactDeparturesOnly()) {
int routeIndex = routeStop.transitRouteIndex;
RRoute route = this.data.routes[routeIndex];
int currentDepartureIndex = findNextDepartureIndex(route, routeStop, (int) depTime);
if (currentDepartureIndex >= 0) {
Vehicle currentVehicle = this.data.departureVehicles[currentDepartureIndex];
int firstDepartureTime = this.data.departures[currentDepartureIndex];
int stopDepartureTime = firstDepartureTime + routeStop.departureOffset;
useStop = Math.abs(depTime - stopDepartureTime) < 1e-5;
Expand Down Expand Up @@ -692,8 +701,11 @@ private void exploreRoutes(RaptorParameters parameters, Person person, CachingTr
transferProvider.reset(boardingPE.transfer);

for (int toRouteStopIndex = firstRouteStopIndex + 1; toRouteStopIndex < route.indexFirstRouteStop + route.countRouteStops; toRouteStopIndex++) {
routeSegmentIterator.reset(currentDepartureIndex, currentAgentBoardingTime, currentBoardingRouteStopIndex, toRouteStopIndex);
RRouteStop toRouteStop = this.data.routeStops[toRouteStopIndex];
if (!toRouteStop.routeStop.isAllowAlighting()) {
continue;
}
this.routeSegmentIterator.reset(currentDepartureIndex, currentAgentBoardingTime, currentBoardingRouteStopIndex, toRouteStopIndex);
int arrivalTime = currentDepartureTime + toRouteStop.arrivalOffset;
int inVehicleTime = arrivalTime - currentAgentBoardingTime;
double inVehicleCost = this.inVehicleCostCalculator.getInVehicleCost(inVehicleTime, marginalUtilityOfTravelTime_utl_s, person, currentVehicle, parameters, routeSegmentIterator);
Expand Down Expand Up @@ -814,17 +826,17 @@ private void handleTransfers(boolean strict, RaptorParameters raptorParams, Cach
continue;
}
RRouteStop fromRouteStop = fromPE.toRouteStop; // this is the route stop we arrive with least cost at stop

// obtain on-demand transfers if applicable (will return null if transfers are calculated initially)
RTransfer[] transfers = this.data.calculateTransfers(fromRouteStop);

int firstTransferIndex = transfers == null ? fromRouteStop.indexFirstTransfer : 0;
int lastTransferIndex = transfers == null ? firstTransferIndex + fromRouteStop.countTransfers : transfers.length;
transfers = transfers == null ? this.data.transfers : transfers;

for (int transferIndex = firstTransferIndex; transferIndex < lastTransferIndex; transferIndex++) {
RTransfer transfer = transfers[transferIndex];

int toRouteStopIndex = transfer.toRouteStop;
transferProvider.reset(transfer);
int newArrivalTime = arrivalTime + transfer.transferTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class SwissRailRaptorData {
final QuadTree<TransitStopFacility> stopsQT;
final Map<String, Map<String, QuadTree<TransitStopFacility>>> stopFilterAttribute2Value2StopsQT;
final OccupancyData occupancyData;

// data needed if cached transfer construction is activated
final IdMap<TransitStopFacility, Map<TransitStopFacility, Double>> staticTransferTimes;
final RTransfer[][] transferCache;
Expand All @@ -105,7 +105,7 @@ private SwissRailRaptorData(RaptorStaticConfig config, int countStops,
this.stopsQT = stopsQT;
this.stopFilterAttribute2Value2StopsQT = new HashMap<>();
this.occupancyData = occupancyData;

// data needed if cached transfer construction is activated
this.staticTransferTimes = staticTransferTimes;
this.transferCache = new RTransfer[routeStops.length][];
Expand Down Expand Up @@ -216,7 +216,7 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh

// if cached transfer calculation is active, don't generate any transfers here
final Map<Integer, RTransfer[]> allTransfers;

if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Initial)) {
allTransfers = calculateRouteStopTransfers(schedule, stopsQT, routeStopsPerStopFacility, routeStops,
staticConfig);
Expand Down Expand Up @@ -244,7 +244,7 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh
indexTransfer += transferCount;
}
}

// if cached transfer calculation is used, build a map for quick lookup of minimal transfer times
IdMap<TransitStopFacility, Map<TransitStopFacility, Double>> staticTransferTimes = null;
if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Cached)) {
Expand All @@ -258,7 +258,7 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh
.put(schedule.getFacilities().get(iterator.getToStopId()), iterator.getSeconds());
}
}

SwissRailRaptorData data = new SwissRailRaptorData(staticConfig, countStopFacilities, routes, departures, departureVehicles, departureIds, routeStops, transfers, stopFacilityIndices, routeStopsPerStopFacility, stopsQT, occupancyData, staticTransferTimes);

long endMillis = System.currentTimeMillis();
Expand Down Expand Up @@ -327,7 +327,9 @@ private static Map<Integer, RTransfer[]> calculateRouteStopTransfers(TransitSche
stopTransfers.clear();
for (int toRouteStopIndex : toRouteStopIndices) {
RRouteStop toRouteStop = routeStops[toRouteStopIndex];
if (isUsefulTransfer(fromRouteStop, toRouteStop, maxBeelineWalkConnectionDistance, config.getOptimization())) {
if (isUsefulTransfer(fromRouteStop, toRouteStop, maxBeelineWalkConnectionDistance, config.getOptimization())
&& isTransferAllowed(fromRouteStop, toRouteStop)
) {
RTransfer newTransfer = new RTransfer(fromRouteStopIndex, toRouteStopIndex, fixedTransferTime, beelineDistance * beelineDistanceFactor);
stopTransfers.add(newTransfer);
}
Expand Down Expand Up @@ -388,6 +390,10 @@ private static boolean isUsefulTransfer(RRouteStop fromRouteStop, RRouteStop toR
return true;
}

private static boolean isTransferAllowed(RRouteStop fromRouteStop, RRouteStop toRouteStop) {
return fromRouteStop.routeStop.isAllowAlighting() && toRouteStop.routeStop.isAllowBoarding();
}

private static boolean isFirstStopInRoute(RRouteStop routeStop) {
TransitRouteStop firstRouteStop = routeStop.route.getStops().get(0);
return routeStop.routeStop == firstRouteStop;
Expand Down Expand Up @@ -641,12 +647,12 @@ public Transfer get() {
return this.transfer;
}
}

RTransfer[] calculateTransfers(RRouteStop fromRouteStop) {
if (config.getTransferCalculation().equals(RaptorTransferCalculation.Initial)) {
return null;
}

// We tested this in a parallel set-up and things seem to work as they are
// implemented. The routing threads will access the cache as read-only an
// retrieve the cached stop connections. It can happen that two of them try to
Expand All @@ -661,41 +667,41 @@ RTransfer[] calculateTransfers(RRouteStop fromRouteStop) {

RTransfer[] cache = transferCache[fromRouteStop.index];
if (cache != null) return cache; // we had a cache hit

// setting up useful constants
final double minimalTransferTime = config.getMinimalTransferTime();
final double beelineWalkConnectionDistance = config.getBeelineWalkConnectionDistance();
final double beelineDistanceFactor = config.getBeelineWalkDistanceFactor();
final double beelineWalkSpeed = config.getBeelineWalkSpeed();
final RaptorOptimization optimization = config.getOptimization();

// the facility from which we want to transfer
TransitStopFacility fromRouteFacility = fromRouteStop.routeStop.getStopFacility();
Collection<TransitStopFacility> transferCandidates = new LinkedList<>();

// find transfer candidates by distance
transferCandidates.addAll(stopsQT.getDisk(fromRouteFacility.getCoord().getX(), fromRouteFacility.getCoord().getY(), config.getBeelineWalkConnectionDistance()));

// find transfer candidates with predefined transfer time
Map<TransitStopFacility, Double> transferTimes = staticTransferTimes.get(fromRouteFacility.getId());

if (transferTimes != null) { // some transfer times are predefined
transferCandidates.addAll(transferTimes.keySet());
}

// now evaluate whether transfers are useful, distance, and travel time
List<RTransfer> transfers = new LinkedList<>();
for (TransitStopFacility toRouteFacility : transferCandidates) {
for (int toRouteStopIndex : routeStopsPerStopFacility.get(toRouteFacility)) {
RRouteStop toRouteStop = routeStops[toRouteStopIndex];

double beelineDistance = CoordUtils.calcEuclideanDistance(fromRouteFacility.getCoord(), toRouteFacility.getCoord());
double transferTime = beelineDistance / beelineWalkSpeed;

if (transferTime < minimalTransferTime) {
transferTime = minimalTransferTime;
}

if (transferTimes != null) {
// check if we find a predefined transfer time
transferTime = transferTimes.getOrDefault(toRouteFacility, transferTime);
Expand All @@ -706,10 +712,10 @@ RTransfer[] calculateTransfers(RRouteStop fromRouteStop) {
}
}
}

// convert to array
RTransfer[] stopTransfers = transfers.toArray(new RTransfer[transfers.size()]);

// save to cache (no issue regarding parallel execution because we simply set an element)
transferCache[fromRouteStop.index] = stopTransfers;
return stopTransfers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ abstract class Constants {
static final String VEHICLE_REF_ID = "vehicleRefId";
static final String DEPARTURE_OFFSET = "departureOffset";
static final String ARRIVAL_OFFSET = "arrivalOffset";
static final String ALLOW_BOARDING = "allowBoarding";
static final String ALLOW_ALIGHTING = "allowAlighting";
static final String AWAIT_DEPARTURE = "awaitDeparture";
static final String IS_BLOCKING = "isBlocking";
static final String STOP_AREA_ID = "stopAreaId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ public class TransitRouteStopImpl implements TransitRouteStop {
private final OptionalTime departureOffset;
private final OptionalTime arrivalOffset;
private boolean awaitDepartureTime = false;
private boolean allowBoarding = true;
private boolean allowAlighting = true;

private TransitRouteStopImpl(Builder builder) {
stop = builder.stop;
departureOffset = builder.departureOffset;
arrivalOffset = builder.arrivalOffset;
this.stop = builder.stop;
this.departureOffset = builder.departureOffset;
this.arrivalOffset = builder.arrivalOffset;
this.allowBoarding = builder.allowBoarding;
this.allowAlighting = builder.allowAlighting;
setAwaitDepartureTime(builder.awaitDepartureTime);
}

Expand All @@ -65,6 +69,26 @@ public OptionalTime getArrivalOffset() {
return this.arrivalOffset;
}

@Override
public boolean isAllowBoarding() {
return this.allowBoarding;
}

@Override
public void setAllowBoarding(boolean allowBoarding) {
this.allowBoarding = allowBoarding;
}

@Override
public boolean isAllowAlighting() {
return this.allowAlighting;
}

@Override
public void setAllowAlighting(boolean allowAlighting) {
this.allowAlighting = allowAlighting;
}

@Override
public boolean isAwaitDepartureTime() {
return this.awaitDepartureTime;
Expand All @@ -77,14 +101,13 @@ public void setAwaitDepartureTime(final boolean awaitDepartureTime) {

/**
* TransitRouteStops are typical Value Objects, so we consider two stops equal if they are equal field-wise.
*
*
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TransitRouteStopImpl)) {
if (!(obj instanceof TransitRouteStopImpl other)) {
return false;
}
TransitRouteStopImpl other = (TransitRouteStopImpl) obj;
if (this.stop == null) {
if (other.getStopFacility() != null) {
return false;
Expand All @@ -96,10 +119,16 @@ public boolean equals(Object obj) {
}
if (!this.departureOffset.equals(other.getDepartureOffset())) {
return false;
}
}
if (!this.arrivalOffset.equals(other.getArrivalOffset())) {
return false;
}
if (this.allowBoarding != other.allowBoarding) {
return false;
}
if (this.allowAlighting != other.allowAlighting) {
return false;
}
if (this.awaitDepartureTime != other.isAwaitDepartureTime()) {
return false;
}
Expand All @@ -110,7 +139,7 @@ public boolean equals(Object obj) {
public int hashCode() {
return stop.hashCode();
}

@Override
public String toString() {
return "[TransitRouteStop stop=" + this.stop.getId() + " offset=" + this.departureOffset +" ]";
Expand All @@ -120,6 +149,8 @@ public static final class Builder implements TransitRouteStop.Builder<Builder> {
private TransitStopFacility stop;
private OptionalTime departureOffset = OptionalTime.undefined();
private OptionalTime arrivalOffset = OptionalTime.undefined();
private boolean allowBoarding = true;
private boolean allowAlighting = true;
private boolean awaitDepartureTime;

public Builder() {
Expand Down Expand Up @@ -157,6 +188,16 @@ public Builder arrivalOffset(OptionalTime val) {
return this;
}

public Builder allowBoarding(boolean allowBoarding) {
this.allowBoarding = allowBoarding;
return this;
}

public Builder allowAlighting(boolean allowAlighting) {
this.allowAlighting = allowAlighting;
return this;
}

public Builder awaitDepartureTime(boolean val) {
awaitDepartureTime = val;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ public void startTag(final String name, final Attributes atts, final Stack<Strin
if (departure != null) {
Time.parseOptionalTime(departure).ifDefined(stopBuilder::departureOffset);
}
stopBuilder.allowBoarding(Boolean.parseBoolean(atts.getValue(Constants.ALLOW_BOARDING)));
stopBuilder.allowAlighting(Boolean.parseBoolean(atts.getValue(Constants.ALLOW_ALIGHTING)));
stopBuilder.awaitDepartureTime(Boolean.parseBoolean(atts.getValue(Constants.AWAIT_DEPARTURE)));
this.currentTransitRoute.stopBuilders.add(stopBuilder);
} else if (Constants.RELATION.equals(name)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ private void writeRouteProfile(final List<TransitRouteStop> stops) throws Unchec
.ifDefined(offset -> attributes.add(createTimeTuple(Constants.ARRIVAL_OFFSET, offset)));
stop.getDepartureOffset().ifDefined(offset->
attributes.add(createTimeTuple(Constants.DEPARTURE_OFFSET, offset)));
// do not write out if it is true ==> the default value
if (!stop.isAllowBoarding()) {
attributes.add(createTuple(Constants.ALLOW_BOARDING, String.valueOf(stop.isAllowBoarding())));
}
if (!stop.isAllowAlighting()) {
attributes.add(createTuple(Constants.ALLOW_ALIGHTING, String.valueOf(stop.isAllowAlighting())));
}
attributes.add(createTuple(Constants.AWAIT_DEPARTURE, String.valueOf(stop.isAwaitDepartureTime())));
this.writeStartTag(Constants.STOP, attributes, true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ public interface TransitRouteStop {

public abstract OptionalTime getArrivalOffset();

/** @return <code>true</code> if agents are allowed to board the transit vehicle at this route stop. */
boolean isAllowBoarding();

void setAllowBoarding(boolean allowBoarding);

/** @return <code>true</code> if agents are allowed to exit the transit vehicle at this route stop. */
boolean isAllowAlighting();

void setAllowAlighting(boolean allowAlighting);

/**
* Specifies if a driver should wait until the specified departure time
* has come before departing, especially if the driver is too early at
Expand Down Expand Up @@ -69,6 +79,10 @@ interface Builder<B extends Builder<B>> {

Builder<B> awaitDepartureTime(boolean val);

Builder<B> allowBoarding(boolean val);

Builder<B> allowAlighting(boolean val);

TransitRouteStop build();
}
}
}
Loading

0 comments on commit 2c5374a

Please sign in to comment.