From 2c5374acdca1f3c018de2f5406891d24f87bce3f Mon Sep 17 00:00:00 2001 From: Marcel Rieser Date: Sat, 22 Jun 2024 23:23:12 +0200 Subject: [PATCH] Support TransitRouteStops where boarding or alighting is not allowed 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. --- .../pt/raptor/SwissRailRaptorCore.java | 28 +- .../pt/raptor/SwissRailRaptorData.java | 44 +- .../matsim/pt/transitSchedule/Constants.java | 2 + .../transitSchedule/TransitRouteStopImpl.java | 57 ++- .../TransitScheduleReaderV2.java | 2 + .../TransitScheduleWriterV2.java | 7 + .../transitSchedule/api/TransitRouteStop.java | 16 +- .../main/resources/dtd/transitSchedule_v2.dtd | 2 + ...RaptorRestrictedBoardingAlightingTest.java | 414 ++++++++++++++++++ 9 files changed, 536 insertions(+), 36 deletions(-) create mode 100644 matsim/src/test/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorRestrictedBoardingAlightingTest.java diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java index 4a84ee50c21..3778eebfaea 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java @@ -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; @@ -318,6 +322,9 @@ public List 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++) { @@ -502,13 +509,15 @@ public Map, 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; @@ -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); @@ -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; diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java index 4813fafd95d..7ec137e6096 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java @@ -81,7 +81,7 @@ public class SwissRailRaptorData { final QuadTree stopsQT; final Map>> stopFilterAttribute2Value2StopsQT; final OccupancyData occupancyData; - + // data needed if cached transfer construction is activated final IdMap> staticTransferTimes; final RTransfer[][] transferCache; @@ -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][]; @@ -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 allTransfers; - + if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Initial)) { allTransfers = calculateRouteStopTransfers(schedule, stopsQT, routeStopsPerStopFacility, routeStops, staticConfig); @@ -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> staticTransferTimes = null; if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Cached)) { @@ -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(); @@ -327,7 +327,9 @@ private static Map 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); } @@ -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; @@ -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 @@ -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 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 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 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); @@ -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; diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/Constants.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/Constants.java index 88a55b790f0..8045d019c53 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/Constants.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/Constants.java @@ -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"; diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitRouteStopImpl.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitRouteStopImpl.java index e9123814f35..4193e872511 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitRouteStopImpl.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitRouteStopImpl.java @@ -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); } @@ -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; @@ -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; @@ -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; } @@ -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 +" ]"; @@ -120,6 +149,8 @@ public static final class Builder implements TransitRouteStop.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() { @@ -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; diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitScheduleReaderV2.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitScheduleReaderV2.java index 4c364538f3b..2ef65e449d3 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitScheduleReaderV2.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/TransitScheduleReaderV2.java @@ -175,6 +175,8 @@ public void startTag(final String name, final Attributes atts, final Stack 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); } diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/api/TransitRouteStop.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/api/TransitRouteStop.java index 108e52cb6db..dcc0e51d9cd 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/api/TransitRouteStop.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/api/TransitRouteStop.java @@ -38,6 +38,16 @@ public interface TransitRouteStop { public abstract OptionalTime getArrivalOffset(); + /** @return true if agents are allowed to board the transit vehicle at this route stop. */ + boolean isAllowBoarding(); + + void setAllowBoarding(boolean allowBoarding); + + /** @return true 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 @@ -69,6 +79,10 @@ interface Builder> { Builder awaitDepartureTime(boolean val); + Builder allowBoarding(boolean val); + + Builder allowAlighting(boolean val); + TransitRouteStop build(); } -} \ No newline at end of file +} diff --git a/matsim/src/main/resources/dtd/transitSchedule_v2.dtd b/matsim/src/main/resources/dtd/transitSchedule_v2.dtd index cf3665e0679..5c030f2279c 100644 --- a/matsim/src/main/resources/dtd/transitSchedule_v2.dtd +++ b/matsim/src/main/resources/dtd/transitSchedule_v2.dtd @@ -54,6 +54,8 @@ refId CDATA #REQUIRED departureOffset CDATA #IMPLIED arrivalOffset CDATA #IMPLIED + allowBoarding (true|false) "true" + allowAlighting (true|false) "true" awaitDeparture (true|false) "false">