Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter flex trips by agency and route #6421

Open
wants to merge 26 commits into
base: dev-2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d446bd9
Implement basic flex filtering
leonardehrenfried Jan 24, 2025
7b46c33
Extract reusable method
leonardehrenfried Jan 29, 2025
2760962
Improve logging of large flex areas
leonardehrenfried Jan 29, 2025
4c7c689
Make integration test pass
leonardehrenfried Jan 30, 2025
7f80ad8
Implement separate flex filter logic
leonardehrenfried Jan 31, 2025
17e8d22
Convert record to class, add Javadoc
leonardehrenfried Feb 3, 2025
5f1da2d
Format code
leonardehrenfried Feb 3, 2025
e015ea3
Add Javadoc
leonardehrenfried Feb 3, 2025
cd29ffa
Add test
leonardehrenfried Feb 3, 2025
8b61891
Flesh out tests
leonardehrenfried Feb 3, 2025
0f68794
Re-organise package structure
leonardehrenfried Feb 3, 2025
28fb8dc
Revert changes to AreaStopsToVerticesMapper
leonardehrenfried Feb 3, 2025
0af115d
Add trip based framework filter logic
leonardehrenfried Feb 26, 2025
9d813bf
Add test for filter matching
leonardehrenfried Feb 26, 2025
dc81032
Add test for filter mapping
leonardehrenfried Feb 26, 2025
7bdb1e3
Flesh out filter mapper
leonardehrenfried Feb 26, 2025
f5ac386
Refactor test a little
leonardehrenfried Feb 28, 2025
6931b09
Add Javadoc
leonardehrenfried Feb 28, 2025
2385817
Move filtering to ClosestTrip and add test
leonardehrenfried Feb 28, 2025
1af5aab
Clean up
leonardehrenfried Feb 28, 2025
20a4380
Merge remote-tracking branch 'upstream/dev-2.x' into flex-filtering
leonardehrenfried Mar 6, 2025
16417d4
Format with new prettier version
leonardehrenfried Mar 6, 2025
b007b4c
Move creation of filter values into builder
leonardehrenfried Mar 6, 2025
61048ab
Remove string constants
leonardehrenfried Mar 6, 2025
36a264f
Use correct behaviour for empty list
leonardehrenfried Mar 6, 2025
0ba6f08
Apply review comments
leonardehrenfried Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.routing.api.RoutingService;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.framework.TimeAndCostPenalty;
import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter;
import org.opentripplanner.routing.graph.Graph;
Expand Down Expand Up @@ -236,12 +237,16 @@ private static List<Itinerary> getItineraries(
);

var modes = request.journey().modes().copyOf();
modes.withEgressMode(FLEXIBLE);

if (onlyDirect) {
modes.withDirectMode(FLEXIBLE);
request.journey().transit().setFilters(List.of(ExcludeAllTransitFilter.of()));
modes
.withDirectMode(FLEXIBLE)
.withAccessMode(StreetMode.WALK)
.withEgressMode(StreetMode.WALK);
request.journey().transit().setFilters(List.of(AllowAllTransitFilter.of()));
request.journey().transit().disable();
} else {
modes.withEgressMode(FLEXIBLE);
request.journey().transit().setFilters(List.of(AllowAllTransitFilter.of()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.opentripplanner.ext.flex.filter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;

import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter;
import org.opentripplanner.routing.api.request.request.filter.SelectRequest;
import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest;
import org.opentripplanner.transit.api.request.TripRequest;
import org.opentripplanner.transit.model.framework.FeedScopedId;

class FilterMapperTest {

private static final TripRequest ALLOW_ALL = TripRequest.of().build();
public static final FeedScopedId ROUTE_ID1 = id("r1");
public static final FeedScopedId AGENCY_ID1 = id("a1");

@Test
void allowAll() {
var filter = FilterMapper.map(List.of(AllowAllTransitFilter.of()));
assertEquals(ALLOW_ALL, filter);
}

@Test
void routes() {
var select = SelectRequest.of().withRoutes(List.of(ROUTE_ID1)).build();
var transitFilter = TransitFilterRequest.of().addSelect(select).addNot(select).build();
var actual = FilterMapper.map(List.of(transitFilter));
var expected = TripRequest.of()
.withIncludedRoutes(Set.of(ROUTE_ID1))
.withExcludedRoutes(Set.of(ROUTE_ID1))
.build();

assertEquals(expected, actual);
}

@Test
void agencies() {
var select = SelectRequest.of().withAgencies(List.of(AGENCY_ID1)).build();
var transitFilter = TransitFilterRequest.of().addSelect(select).addNot(select).build();
var actual = FilterMapper.map(List.of(transitFilter));
var expected = TripRequest.of()
.withIncludedAgencies(Set.of(AGENCY_ID1))
.withExcludedAgencies(Set.of(AGENCY_ID1))
.build();

assertEquals(expected, actual);
}

@Test
void combinations() {
var selectRoutes = SelectRequest.of().withRoutes(List.of(ROUTE_ID1)).build();
var routeFilter = TransitFilterRequest.of()
.addSelect(selectRoutes)
.addNot(selectRoutes)
.build();
var selectAgencies = SelectRequest.of().withAgencies(List.of(AGENCY_ID1)).build();
var agencyFilter = TransitFilterRequest.of()
.addSelect(selectAgencies)
.addNot(selectAgencies)
.build();

var actual = FilterMapper.map(List.of(routeFilter, agencyFilter));
var expected = TripRequest.of()
.withIncludedAgencies(Set.of(AGENCY_ID1))
.withExcludedAgencies(Set.of(AGENCY_ID1))
.withIncludedRoutes(Set.of(ROUTE_ID1))
.withExcludedRoutes(Set.of(ROUTE_ID1))
.build();

assertEquals(expected, actual);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package org.opentripplanner.ext.flex.template;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;

import gnu.trove.set.hash.TIntHashSet;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.flex.trip.FlexTrip;
import org.opentripplanner.ext.flex.trip.UnscheduledTrip;
import org.opentripplanner.model.PathTransfer;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.transit.api.model.FilterValues;
import org.opentripplanner.transit.api.request.TripRequest;
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
import org.opentripplanner.transit.model.filter.expr.Matcher;
import org.opentripplanner.transit.model.filter.transit.TripMatcherFactory;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.utils.time.ServiceDateUtils;

class ClosestTripTest {

private static final UnscheduledTrip FLEX_TRIP = UnscheduledTrip.of(id("123"))
.withStopTimes(List.of(area("10:00", "11:00"), area("10:00", "11:00")))
.withTrip(TimetableRepositoryForTest.trip("123").build())
.build();

private static final LocalDate DATE = LocalDate.of(2025, 2, 28);
private static final FlexServiceDate FSD = new FlexServiceDate(
DATE,
ServiceDateUtils.secondsSinceStartOfTime(DATE.atStartOfDay(ZoneIds.BERLIN), DATE),
10,
new TIntHashSet()
);
private static final StopLocation STOP = FLEX_TRIP.getStop(0);
private static final FlexAccessEgressCallbackAdapter ADAPTER =
new FlexAccessEgressCallbackAdapter() {
@Override
public TransitStopVertex getStopVertexForStopId(FeedScopedId id) {
return null;
}

@Override
public Collection<PathTransfer> getTransfersFromStop(StopLocation stop) {
return List.of();
}

@Override
public Collection<PathTransfer> getTransfersToStop(StopLocation stop) {
return List.of();
}

@Override
public Collection<FlexTrip<?, ?>> getFlexTripsByStop(StopLocation stopLocation) {
return List.of(FLEX_TRIP);
}

@Override
public boolean isDateActive(FlexServiceDate date, FlexTrip<?, ?> trip) {
return true;
}
};

@Test
void doNotFilter() {
var request = TripRequest.of().build();
var matcher = TripMatcherFactory.of(request, id -> Set.of(DATE));

var trips = closestTrips(matcher);
assertThat(trips).hasSize(1);
assertEquals(List.copyOf(trips).getFirst().flexTrip(), FLEX_TRIP);
}

@Test
void filter() {
var request = TripRequest.of()
.withExcludedAgencies(List.of(FLEX_TRIP.getTrip().getRoute().getAgency().getId()))
.build();

var matcher = TripMatcherFactory.of(request, id -> Set.of(DATE));

var trips = closestTrips(matcher);
assertThat(trips).isEmpty();
}

private static Collection<ClosestTrip> closestTrips(Matcher<Trip> matcher) {
return ClosestTrip.of(
ADAPTER,
List.of(new NearbyStop(STOP, 100, List.of(), null)),
matcher,
List.of(FSD),
true
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.opentripplanner.street.model.vertex.StreetLocation;
import org.opentripplanner.street.search.request.StreetSearchRequest;
import org.opentripplanner.street.search.state.State;
import org.opentripplanner.transit.api.request.TripRequest;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService;
import org.opentripplanner.transit.model.site.AreaStop;
Expand Down Expand Up @@ -104,6 +105,7 @@ void calculateDirectFare() {
graph,
new DefaultTransitService(timetableRepository),
FlexParameters.defaultValues(),
TripRequest.of().build(),
OffsetDateTime.parse("2021-11-12T10:15:24-05:00").toInstant(),
null,
1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ public Collection<PathTransfer> getTransfersFromStop(StopLocation stopLocation)
return flexTripsByStop.get(stopLocation);
}

public Route getRouteById(FeedScopedId id) {
return routeById.get(id);
}

public boolean contains(Route route) {
return routeById.containsKey(route.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.transit.api.request.TripRequest;
import org.opentripplanner.transit.model.filter.expr.Matcher;
import org.opentripplanner.transit.model.filter.transit.TripMatcherFactory;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo;
import org.opentripplanner.transit.service.TransitService;
import org.opentripplanner.utils.time.ServiceDateUtils;
Expand All @@ -52,11 +56,13 @@ public class FlexRouter {
private final int requestedTime;
private final int requestedBookingTime;
private final List<FlexServiceDate> dates;
private final Matcher<Trip> matcher;

public FlexRouter(
Graph graph,
TransitService transitService,
FlexParameters flexParameters,
TripRequest filterRequest,
Instant requestedTime,
@Nullable Instant requestedBookingTime,
int additionalPastSearchDays,
Expand All @@ -70,6 +76,10 @@ public FlexRouter(
this.streetAccesses = streetAccesses;
this.streetEgresses = egressTransfers;
this.flexIndex = transitService.getFlexIndex();
this.matcher = TripMatcherFactory.of(
filterRequest,
transitService.getCalendarService()::getServiceDatesForServiceId
);
this.callbackService = new CallbackAdapter();
this.graphPathToItineraryMapper = new GraphPathToItineraryMapper(
transitService.getTimeZone(),
Expand Down Expand Up @@ -115,7 +125,8 @@ public List<Itinerary> createFlexOnlyItineraries(boolean arriveBy) {
callbackService,
accessFlexPathCalculator,
egressFlexPathCalculator,
flexParameters.maxTransferDuration()
flexParameters.maxTransferDuration(),
matcher
).calculateDirectFlexPaths(streetAccesses, streetEgresses, dates, requestedTime, arriveBy);

var itineraries = new ArrayList<Itinerary>();
Expand All @@ -139,7 +150,8 @@ public Collection<FlexAccessEgress> createFlexAccesses() {
return new FlexAccessFactory(
callbackService,
accessFlexPathCalculator,
flexParameters.maxTransferDuration()
flexParameters.maxTransferDuration(),
matcher
).createFlexAccesses(streetAccesses, dates);
}

Expand All @@ -148,7 +160,8 @@ public Collection<FlexAccessEgress> createFlexEgresses() {
return new FlexEgressFactory(
callbackService,
egressFlexPathCalculator,
flexParameters.maxTransferDuration()
flexParameters.maxTransferDuration(),
matcher
).createFlexEgresses(streetEgresses, dates);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.opentripplanner.ext.flex.filter;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.opentripplanner.model.modes.ExcludeAllTransitFilter;
import org.opentripplanner.routing.api.request.request.filter.AllowAllTransitFilter;
import org.opentripplanner.routing.api.request.request.filter.TransitFilter;
import org.opentripplanner.routing.api.request.request.filter.TransitFilterRequest;
import org.opentripplanner.transit.api.model.FilterValues;
import org.opentripplanner.transit.api.request.TripRequest;
import org.opentripplanner.transit.model.framework.FeedScopedId;

/**
* Map the internal OTP filter API into the reduced, flex-specific version of it.
*/
public class FilterMapper {

private final Set<FeedScopedId> excludedAgencies = new HashSet<>();
private final Set<FeedScopedId> excludedRoutes = new HashSet<>();
private final Set<FeedScopedId> selectedAgencies = new HashSet<>();
private final Set<FeedScopedId> selectedRoutes = new HashSet<>();

private FilterMapper() {}

public static TripRequest map(List<TransitFilter> filters) {
var mapper = new FilterMapper();
return mapper.mapFilters(filters);
}

private TripRequest mapFilters(List<TransitFilter> filters) {
var builder = TripRequest.of();

for (TransitFilter filter : filters) {
if (filter instanceof TransitFilterRequest sr) {
addFilter(sr);
} else if (
!(filter instanceof AllowAllTransitFilter) && !(filter instanceof ExcludeAllTransitFilter)
) {
throw new IllegalStateException("Unexpected value: " + filter);
}
}
if (!selectedAgencies.isEmpty()) {
builder.withIncludedAgencies(selectedAgencies);
}
if (!selectedRoutes.isEmpty()) {
builder.withIncludedRoutes(selectedRoutes);
}
if (!excludedAgencies.isEmpty()) {
builder.withExcludedAgencies(excludedAgencies);
}
if (!excludedRoutes.isEmpty()) {
builder.withExcludedRoutes(excludedRoutes);
}
return builder.build();
}

private void addFilter(TransitFilterRequest sr) {
sr
.not()
.forEach(s -> {
excludedRoutes.addAll(s.routes());
excludedAgencies.addAll(s.agencies());
});
sr
.select()
.forEach(s -> {
selectedRoutes.addAll(s.routes());
selectedAgencies.addAll(s.agencies());
});
}
}
Loading
Loading