Skip to content

Commit

Permalink
Merge pull request #5222 from HSLdevcom/removetransitifstreetonlyisbe…
Browse files Browse the repository at this point in the history
…tterfilter-costfunc

Remove transit with higher cost than best on-street itinerary filter
  • Loading branch information
leonardehrenfried authored Sep 14, 2023
2 parents 260c581 + 200ed4e commit 429582e
Show file tree
Hide file tree
Showing 14 changed files with 430 additions and 153 deletions.
269 changes: 144 additions & 125 deletions docs/RouteRequest.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/RouterConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ HTTP headers to add to the request. Any header key, value can be inserted.
"costLimitFunction" : "15m + 1.5 x",
"intervalRelaxFactor" : 0.4
},
"nonTransitGeneralizedCostLimit" : "400 + 1.5x",
"removeTransitWithHigherCostThanBestOnStreetOnly" : "60 + 1.3x",
"bikeRentalDistanceRatio" : 0.3,
"accessibilityScore" : true,
"minBikeParkingDistance" : 300,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveItinerariesWithShortStreetLeg;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveParkAndRideWithMostlyWalkingFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfStreetOnlyIsBetterFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfWalkingIsBetterFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveWalkOnlyFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.TransitGeneralizedCostFilter;
import org.opentripplanner.routing.algorithm.filterchain.filter.DeletionFlaggingFilter;
Expand Down Expand Up @@ -56,7 +57,7 @@ public class ItineraryListFilterChainBuilder {
private ItineraryFilterDebugProfile debug = ItineraryFilterDebugProfile.OFF;
private int maxNumberOfItineraries = NOT_SET;
private ListSection maxNumberOfItinerariesCrop = ListSection.TAIL;
private boolean removeTransitWithHigherCostThanBestOnStreetOnly = true;
private CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly;
private boolean removeWalkAllTheWayResults;
private boolean sameFirstOrLastTripFilter;
private TransitGeneralizedCostFilterParams transitGeneralizedCostFilterParams;
Expand All @@ -72,6 +73,7 @@ public class ItineraryListFilterChainBuilder {
private Function<Station, MultiModalStation> getMultiModalStation;
private boolean removeItinerariesWithSameRoutesAndStops;
private double minBikeParkingDistance;
private boolean removeTransitIfWalkingIsBetter = true;

@Sandbox
private ItineraryListFilter rideHailingFilter;
Expand Down Expand Up @@ -184,21 +186,32 @@ public ItineraryListFilterChainBuilder withParkAndRideDurationRatio(double value
/**
* The direct street search(walk, bicycle, car) is not pruning the transit search, so in some
* cases we get "silly" transit itineraries that is marginally better on travel-duration compared
* with a on-street-all-the-way itinerary. Use this method to turn this filter on/off.
* with a on-street-all-the-way itinerary. Use this method to filter worse enough itineraries.
* <p>
* The filter remove all itineraries with a generalized-cost that is higher than the best
* The filter removes all itineraries with a generalized-cost that is higher than the best
* on-street-all-the-way itinerary.
* <p>
* This filter only have an effect, if an on-street-all-the-way(WALK, BICYCLE, CAR) itinerary
* exist.
*/
public ItineraryListFilterChainBuilder withRemoveTransitWithHigherCostThanBestOnStreetOnly(
boolean value
CostLinearFunction value
) {
this.removeTransitWithHigherCostThanBestOnStreetOnly = value;
return this;
}

/**
* A transit itinerary with higher generalized-cost than a walk-only itinerary is silly. This filter removes such
* itineraries.
* <p>
* This filter only have an effect, if an walk-all-the-way itinerary exist.
*/
public ItineraryListFilterChainBuilder withRemoveTransitIfWalkingIsBetter(boolean value) {
this.removeTransitIfWalkingIsBetter = value;
return this;
}

/**
* This will NOT delete itineraries, but tag them as deleted using the {@link
* Itinerary#getSystemNotices()}.
Expand Down Expand Up @@ -350,8 +363,19 @@ public ItineraryListFilterChain build() {
// is worse). B is removed by the {@link LatestDepartureTimeFilter} below. This is exactly
// what we want, since both itineraries are none optimal.
{
if (removeTransitWithHigherCostThanBestOnStreetOnly) {
filters.add(new DeletionFlaggingFilter(new RemoveTransitIfStreetOnlyIsBetterFilter()));
// Filter transit itineraries by comparing against non-transit using generalized-cost
if (removeTransitWithHigherCostThanBestOnStreetOnly != null) {
filters.add(
new DeletionFlaggingFilter(
new RemoveTransitIfStreetOnlyIsBetterFilter(
removeTransitWithHigherCostThanBestOnStreetOnly
)
)
);
}

if (removeTransitIfWalkingIsBetter) {
filters.add(new DeletionFlaggingFilter(new RemoveTransitIfWalkingIsBetterFilter()));
}

if (removeWalkAllTheWayResults) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.opentripplanner.model.plan.StreetLeg;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.OutsideSearchWindowFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfStreetOnlyIsBetterFilter;
import org.opentripplanner.routing.algorithm.filterchain.deletionflagger.RemoveTransitIfWalkingIsBetterFilter;
import org.opentripplanner.routing.api.response.RoutingError;

/**
Expand Down Expand Up @@ -50,7 +51,16 @@ static List<RoutingError> computeErrors(
.getSystemNotices()
.stream()
.anyMatch(notice -> notice.tag.equals(RemoveTransitIfStreetOnlyIsBetterFilter.TAG));
if (filteredItineraries.stream().allMatch(isOnStreetAllTheWay.or(isWorseThanStreet))) {
Predicate<Itinerary> isWorseThanWalking = it ->
it
.getSystemNotices()
.stream()
.anyMatch(notice -> notice.tag.equals(RemoveTransitIfWalkingIsBetterFilter.TAG));
if (
filteredItineraries
.stream()
.allMatch(isOnStreetAllTheWay.or(isWorseThanStreet).or(isWorseThanWalking))
) {
var nonTransitIsWalking = filteredItineraries
.stream()
.flatMap(Itinerary::getStreetLegs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.Collectors;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;

/**
* Filter itineraries based on generalizedCost, compared with a on-street-all-the-way itinerary(if
* it exist). If an itinerary is slower than the best all-the-way-on-street itinerary, then the
* it exist). If an itinerary cost exceeds the limit computed from the best all-the-way-on-street itinerary, then the
* transit itinerary is removed.
*/
public class RemoveTransitIfStreetOnlyIsBetterFilter implements ItineraryDeletionFlagger {

private final CostLinearFunction costLimitFunction;

public RemoveTransitIfStreetOnlyIsBetterFilter(CostLinearFunction costLimitFunction) {
this.costLimitFunction = costLimitFunction;
}

/**
* Required for {@link org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain},
* to know which filters removed
Expand All @@ -26,19 +36,22 @@ public String name() {

@Override
public List<Itinerary> flagForRemoval(List<Itinerary> itineraries) {
// Find the best walk-all-the-way option
Optional<Itinerary> bestStreetOp = itineraries
// Find the best street-all-the-way option
OptionalInt minStreetCost = itineraries
.stream()
.filter(Itinerary::isOnStreetAllTheWay)
.min(Comparator.comparingInt(Itinerary::getGeneralizedCost));
.mapToInt(Itinerary::getGeneralizedCost)
.min();

if (bestStreetOp.isEmpty()) {
if (minStreetCost.isEmpty()) {
return List.of();
}

final long limit = bestStreetOp.get().getGeneralizedCost();
var limit = costLimitFunction
.calculate(Cost.costOfSeconds(minStreetCost.getAsInt()))
.toSeconds();

// Filter away itineraries that have higher cost than the best non-transit option.
// Filter away itineraries that have higher cost than limit cost computed above
return itineraries
.stream()
.filter(it -> !it.isOnStreetAllTheWay() && it.getGeneralizedCost() >= limit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.opentripplanner.routing.algorithm.filterchain.deletionflagger;

import java.util.Comparator;
import java.util.List;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.Collectors;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;

/**
* Filter itineraries which have a higher generalized-cost than a pure walk itinerary.
*/
public class RemoveTransitIfWalkingIsBetterFilter implements ItineraryDeletionFlagger {

/**
* Required for {@link org.opentripplanner.routing.algorithm.filterchain.ItineraryListFilterChain},
* to know which filters removed
*/
public static final String TAG = "transit-vs-walk-filter";

@Override
public String name() {
return TAG;
}

@Override
public List<Itinerary> flagForRemoval(List<Itinerary> itineraries) {
OptionalInt minWalkCost = itineraries
.stream()
.filter(Itinerary::isWalkingAllTheWay)
.mapToInt(Itinerary::getGeneralizedCost)
.min();

if (minWalkCost.isEmpty()) {
return List.of();
}

var limit = minWalkCost.getAsInt();

return itineraries
.stream()
.filter(it -> !it.isOnStreetAllTheWay() && it.getGeneralizedCost() >= limit)
.collect(Collectors.toList());
}

@Override
public boolean skipAlreadyFlaggedItineraries() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public static ItineraryListFilterChain createFilterChain(
.withBikeRentalDistanceRatio(params.bikeRentalDistanceRatio())
.withParkAndRideDurationRatio(params.parkAndRideDurationRatio())
.withNonTransitGeneralizedCostLimit(params.nonTransitGeneralizedCostLimit())
.withRemoveTransitWithHigherCostThanBestOnStreetOnly(
params.removeTransitWithHigherCostThanBestOnStreetOnly()
)
.withSameFirstOrLastTripFilter(params.filterItinerariesWithSameFirstOrLastTrip())
.withAccessibilityScore(
params.useAccessibilityScore() && request.wheelchair(),
Expand All @@ -79,10 +82,10 @@ public static ItineraryListFilterChain createFilterChain(
context.transitService().getTransitAlertService(),
context.transitService()::getMultiModalStationForStation
)
.withRemoveTransitWithHigherCostThanBestOnStreetOnly(true)
.withLatestDepartureTimeLimit(filterOnLatestDepartureTime)
.withMaxLimitReachedSubscriber(maxLimitReachedSubscriber)
.withRemoveWalkAllTheWayResults(removeWalkAllTheWayResults)
.withRemoveTransitIfWalkingIsBetter(true)
.withDebugEnabled(params.debug());

if (!context.rideHailingServices().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public final class ItineraryFilterPreferences {
private final double parkAndRideDurationRatio;
private final boolean removeItinerariesWithSameRoutesAndStops;
private final TransitGeneralizedCostFilterParams transitGeneralizedCostLimit;
private final CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly;
private final boolean removeTransitIfWalkingIsBetter;

private ItineraryFilterPreferences() {
this.accessibilityScore = false;
Expand All @@ -48,6 +50,9 @@ private ItineraryFilterPreferences() {
CostLinearFunction.of(Duration.ofMinutes(15), 1.5),
0.4
);
this.removeTransitWithHigherCostThanBestOnStreetOnly =
CostLinearFunction.of(Duration.ofMinutes(1), 1.3);
this.removeTransitIfWalkingIsBetter = false;
}

private ItineraryFilterPreferences(Builder builder) {
Expand All @@ -66,6 +71,9 @@ private ItineraryFilterPreferences(Builder builder) {
this.parkAndRideDurationRatio = Units.ratio(builder.parkAndRideDurationRatio);
this.removeItinerariesWithSameRoutesAndStops = builder.removeItinerariesWithSameRoutesAndStops;
this.transitGeneralizedCostLimit = Objects.requireNonNull(builder.transitGeneralizedCostLimit);
this.removeTransitWithHigherCostThanBestOnStreetOnly =
Objects.requireNonNull(builder.removeTransitWithHigherCostThanBestOnStreetOnly);
this.removeTransitIfWalkingIsBetter = builder.removeTransitIfWalkingIsBetter;
}

public static Builder of() {
Expand Down Expand Up @@ -124,6 +132,14 @@ public TransitGeneralizedCostFilterParams transitGeneralizedCostLimit() {
return transitGeneralizedCostLimit;
}

public CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly() {
return removeTransitWithHigherCostThanBestOnStreetOnly;
}

public boolean removeTransitIfWalkingIsBetter() {
return removeTransitIfWalkingIsBetter;
}

@Override
public String toString() {
return ToStringBuilder
Expand Down Expand Up @@ -162,10 +178,16 @@ public String toString() {
transitGeneralizedCostLimit,
DEFAULT.transitGeneralizedCostLimit
)
.addObj(
"removeTransitWithHigherCostThanBestOnStreetOnly",
removeTransitWithHigherCostThanBestOnStreetOnly,
DEFAULT.removeTransitWithHigherCostThanBestOnStreetOnly
)
.addBoolIfTrue(
"removeItinerariesWithSameRoutesAndStops",
removeItinerariesWithSameRoutesAndStops
)
.addBoolIfTrue("removeTransitIfWalkingIsBetter", removeTransitIfWalkingIsBetter)
.toString();
}

Expand All @@ -178,6 +200,7 @@ public boolean equals(Object o) {
accessibilityScore == that.accessibilityScore &&
Double.compare(that.bikeRentalDistanceRatio, bikeRentalDistanceRatio) == 0 &&
debug == that.debug &&
removeTransitIfWalkingIsBetter == that.removeTransitIfWalkingIsBetter &&
filterItinerariesWithSameFirstOrLastTrip == that.filterItinerariesWithSameFirstOrLastTrip &&
Double.compare(
that.groupedOtherThanSameLegsMaxCostMultiplier,
Expand All @@ -190,6 +213,10 @@ public boolean equals(Object o) {
Double.compare(that.parkAndRideDurationRatio, parkAndRideDurationRatio) == 0 &&
removeItinerariesWithSameRoutesAndStops == that.removeItinerariesWithSameRoutesAndStops &&
Objects.equals(nonTransitGeneralizedCostLimit, that.nonTransitGeneralizedCostLimit) &&
Objects.equals(
removeTransitWithHigherCostThanBestOnStreetOnly,
that.removeTransitWithHigherCostThanBestOnStreetOnly
) &&
Objects.equals(transitGeneralizedCostLimit, that.transitGeneralizedCostLimit)
);
}
Expand All @@ -208,7 +235,9 @@ public int hashCode() {
nonTransitGeneralizedCostLimit,
parkAndRideDurationRatio,
removeItinerariesWithSameRoutesAndStops,
transitGeneralizedCostLimit
transitGeneralizedCostLimit,
removeTransitWithHigherCostThanBestOnStreetOnly,
removeTransitIfWalkingIsBetter
);
}

Expand All @@ -227,6 +256,8 @@ public static class Builder {
private double parkAndRideDurationRatio;
private boolean removeItinerariesWithSameRoutesAndStops;
private TransitGeneralizedCostFilterParams transitGeneralizedCostLimit;
private CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly;
private boolean removeTransitIfWalkingIsBetter;

public ItineraryFilterPreferences original() {
return original;
Expand Down Expand Up @@ -302,6 +333,19 @@ public Builder withTransitGeneralizedCostLimit(
return this;
}

public Builder withRemoveTransitWithHigherCostThanBestOnStreetOnly(
CostLinearFunction removeTransitWithHigherCostThanBestOnStreetOnly
) {
this.removeTransitWithHigherCostThanBestOnStreetOnly =
removeTransitWithHigherCostThanBestOnStreetOnly;
return this;
}

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

public Builder(ItineraryFilterPreferences original) {
this.original = original;
this.accessibilityScore = original.accessibilityScore;
Expand All @@ -319,6 +363,9 @@ public Builder(ItineraryFilterPreferences original) {
this.removeItinerariesWithSameRoutesAndStops =
original.removeItinerariesWithSameRoutesAndStops;
this.transitGeneralizedCostLimit = original.transitGeneralizedCostLimit;
this.removeTransitWithHigherCostThanBestOnStreetOnly =
original.removeTransitWithHigherCostThanBestOnStreetOnly;
this.removeTransitIfWalkingIsBetter = original.removeTransitIfWalkingIsBetter;
}

public Builder apply(Consumer<Builder> body) {
Expand Down
Loading

0 comments on commit 429582e

Please sign in to comment.