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 routes and patterns by service date in GTFS GraphQL API #5869

Merged
merged 46 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3ca1a0c
Implement query for filtering by service days
leonardehrenfried May 23, 2024
c3cb2d6
Move filtering logic into class
leonardehrenfried May 24, 2024
775a776
Add test for range handling
leonardehrenfried May 24, 2024
7e66dfd
Add tests
leonardehrenfried May 24, 2024
cbe912d
Flesh out tests
leonardehrenfried May 24, 2024
d6b568f
Rename service day to service date
leonardehrenfried May 24, 2024
3f0d492
Add example for LocalDate
leonardehrenfried May 24, 2024
28b078d
Update test
leonardehrenfried May 24, 2024
e6971b4
Add more test cases
leonardehrenfried May 27, 2024
244da3f
Update test case for edge case
leonardehrenfried May 27, 2024
994d0b5
Be strict about accepting RFC3339
leonardehrenfried Jun 4, 2024
782964a
Update documenation about service date
leonardehrenfried Jun 4, 2024
1734b8a
Clarify which date formats are allowed
leonardehrenfried Jun 10, 2024
9cfc1d7
Allow overriding of description
leonardehrenfried Jun 10, 2024
e4a664b
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jun 10, 2024
591405c
Instantiate real service instead of mocking it
leonardehrenfried Jun 10, 2024
5d29a44
Rename argument provider methods
leonardehrenfried Jun 10, 2024
b618e71
Pass in functions rather than the whole transit service
leonardehrenfried Jun 11, 2024
93f400b
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jun 11, 2024
4ad42e7
Resolve merge conflicts
leonardehrenfried Jun 11, 2024
df587e3
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jun 11, 2024
3c3bd18
Throw exception if both start/end are null
leonardehrenfried Jun 11, 2024
0a8d03c
Rename and deprecate
leonardehrenfried Jun 11, 2024
d5ea5af
Extract two separate methods for Transmodel and GTFS scalar versions
leonardehrenfried Jun 17, 2024
b77d50b
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jun 17, 2024
54b2fe2
Refer to API documentation
leonardehrenfried Jun 17, 2024
665ce7a
Move small helper method into separate class
leonardehrenfried Jun 17, 2024
f549d05
Mention new filter engine
leonardehrenfried Jun 18, 2024
ee4887e
Switch to endExclusive
leonardehrenfried Jun 18, 2024
e754695
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jun 25, 2024
13d55c2
Rename ServiceDateFilterInput to LocalDateRangeInput
leonardehrenfried Jul 1, 2024
b21dd60
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jul 1, 2024
0edc190
Move logic into mapper, extract separate class for date range
leonardehrenfried Jul 1, 2024
a83a9e8
Update src/main/java/org/opentripplanner/apis/gtfs/PatternByServiceDa…
leonardehrenfried Jul 2, 2024
b88a35e
Update error message
leonardehrenfried Jul 2, 2024
d62afd4
Merge remote-tracking branch 'upstream/dev-2.x' into route-filter
leonardehrenfried Jul 3, 2024
2e22b5e
Clean up after merge
leonardehrenfried Jul 3, 2024
67c3279
Fix indentation
leonardehrenfried Jul 3, 2024
361ff91
Move helper code into utils
leonardehrenfried Jul 4, 2024
b6de830
Rename method
leonardehrenfried Jul 5, 2024
614434f
Move code inside date range in order to remove feature envy
leonardehrenfried Jul 5, 2024
b617f95
Rework test
leonardehrenfried Jul 8, 2024
ae363f4
Update src/main/java/org/opentripplanner/apis/gtfs/model/LocalDateRan…
leonardehrenfried Jul 24, 2024
1ebfc42
Format code
leonardehrenfried Jul 24, 2024
80fedfa
Make method private
leonardehrenfried Jul 25, 2024
3cf6130
Make method private
leonardehrenfried Jul 25, 2024
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
2 changes: 1 addition & 1 deletion magidoc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ To learn how to deactivate it, read the
'Polyline': '<>',
'GeoJson': '<>',
'OffsetDateTime': '2024-02-05T18:04:23+01:00',
'LocalDate': '2024-05-24',
'Duration': 'PT10M',
'CoordinateValue': 19.24,
'Reluctance': 3.1,
'Speed': 3.4,
'Cost': 100,
'Ratio': 0.25,
'Locale': 'en'

},
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.Optional;
import javax.annotation.Nonnull;
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.framework.graphql.scalar.DateScalarFactory;
import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory;
import org.opentripplanner.framework.json.ObjectMappers;
import org.opentripplanner.framework.model.Cost;
Expand Down Expand Up @@ -235,6 +236,8 @@ private static Optional<Cost> validateCost(int cost) {
)
.build();

public static final GraphQLScalarType LOCAL_DATE_SCALAR = DateScalarFactory.createGtfsDateScalar();

public static final GraphQLScalarType GEOJSON_SCALAR = GraphQLScalarType
.newScalar()
.name("GeoJson")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.opentripplanner.apis.gtfs.datafetchers.AgencyImpl;
Expand Down Expand Up @@ -86,7 +84,6 @@
import org.opentripplanner.apis.support.graphql.LoggingDataFetcherExceptionHandler;
import org.opentripplanner.ext.actuator.MicrometerGraphQLInstrumentation;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.concurrent.OtpRequestThreadFactory;
import org.opentripplanner.framework.graphql.GraphQLResponseSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -97,10 +94,6 @@ class GtfsGraphQLIndex {

private static final GraphQLSchema indexSchema = buildSchema();

static final ExecutorService threadPool = Executors.newCachedThreadPool(
OtpRequestThreadFactory.of("gtfs-api-%d")
);

protected static GraphQLSchema buildSchema() {
try {
URL url = Objects.requireNonNull(GtfsGraphQLIndex.class.getResource("schema.graphqls"));
Expand All @@ -118,6 +111,7 @@ protected static GraphQLSchema buildSchema() {
.scalar(GraphQLScalars.COORDINATE_VALUE_SCALAR)
.scalar(GraphQLScalars.COST_SCALAR)
.scalar(GraphQLScalars.RELUCTANCE_SCALAR)
.scalar(GraphQLScalars.LOCAL_DATE_SCALAR)
.scalar(ExtendedScalars.GraphQLLong)
.scalar(ExtendedScalars.Locale)
.scalar(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.opentripplanner.apis.gtfs;

import java.time.LocalDate;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
import org.opentripplanner.apis.gtfs.model.LocalDateRange;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.service.TransitService;

/**
* Encapsulates the logic to filter patterns by the service dates that they operate on. It also
* has a method to filter routes by checking if their patterns operate on the required days.
* <p>
* Once a more complete filtering engine is in place in the core data model, this code should be
* there rather than a separate class in the API package.
*/
public class PatternByServiceDatesFilter {

private final Function<Route, Collection<TripPattern>> getPatternsForRoute;
private final Function<Trip, Collection<LocalDate>> getServiceDatesForTrip;
private final LocalDateRange range;

/**
* This method is not private to enable unit testing.
* <p>
*/
PatternByServiceDatesFilter(
LocalDateRange range,
Function<Route, Collection<TripPattern>> getPatternsForRoute,
Function<Trip, Collection<LocalDate>> getServiceDatesForTrip
) {
this.getPatternsForRoute = Objects.requireNonNull(getPatternsForRoute);
this.getServiceDatesForTrip = Objects.requireNonNull(getServiceDatesForTrip);
this.range = range;

if (range.unlimited()) {
throw new IllegalArgumentException("start and end cannot be both null");
} else if (range.startBeforeEnd()) {
throw new IllegalArgumentException("start must be before end");
}
}

public PatternByServiceDatesFilter(
GraphQLTypes.GraphQLLocalDateRangeInput filterInput,
TransitService transitService
) {
this(
new LocalDateRange(filterInput.getGraphQLStart(), filterInput.getGraphQLEnd()),
transitService::getPatternsForRoute,
trip -> transitService.getCalendarService().getServiceDatesForServiceId(trip.getServiceId())
);
}

/**
* Filter the patterns by the service dates that it operates on.
*/
public Collection<TripPattern> filterPatterns(Collection<TripPattern> tripPatterns) {
return tripPatterns.stream().filter(this::hasServicesOnDate).toList();
}

/**
* Filter the routes by listing all their patterns' service dates and checking if they
* operate on the specified dates.
*/
public Collection<Route> filterRoutes(Stream<Route> routeStream) {
return routeStream
.filter(r -> {
var patterns = getPatternsForRoute.apply(r);
return !this.filterPatterns(patterns).isEmpty();
})
.toList();
}

private boolean hasServicesOnDate(TripPattern pattern) {
return pattern
.scheduledTripsAsStream()
.anyMatch(trip -> {
var dates = getServiceDatesForTrip.apply(trip);

return dates
.stream()
.anyMatch(date ->
(
range.startInclusive() == null ||
date.isEqual(range.startInclusive()) ||
date.isAfter(range.startInclusive())
) &&
(range.endExclusive() == null || date.isBefore(range.endExclusive()))
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
import org.locationtech.jts.geom.Envelope;
import org.opentripplanner.apis.gtfs.GraphQLRequestContext;
import org.opentripplanner.apis.gtfs.GraphQLUtils;
import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter;
import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLQueryTypeStopsByRadiusArgs;
import org.opentripplanner.apis.gtfs.mapping.routerequest.LegacyRouteRequestMapper;
import org.opentripplanner.apis.gtfs.mapping.routerequest.RouteRequestMapper;
import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil;
import org.opentripplanner.ext.fares.impl.DefaultFareService;
import org.opentripplanner.ext.fares.impl.GtfsFaresService;
import org.opentripplanner.ext.fares.model.FareRuleSet;
Expand Down Expand Up @@ -609,6 +611,11 @@ public DataFetcher<Iterable<Route>> routes() {
GraphQLUtils.startsWith(route.getLongName(), name, environment.getLocale())
);
}

if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) {
var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService);
routeStream = filter.filterRoutes(routeStream).stream();
}
return routeStream.toList();
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import java.util.stream.Collectors;
import org.opentripplanner.apis.gtfs.GraphQLRequestContext;
import org.opentripplanner.apis.gtfs.GraphQLUtils;
import org.opentripplanner.apis.gtfs.PatternByServiceDatesFilter;
import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode;
import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper;
import org.opentripplanner.apis.gtfs.support.time.LocalDateRangeUtil;
import org.opentripplanner.routing.alertpatch.EntitySelector;
import org.opentripplanner.routing.alertpatch.TransitAlert;
import org.opentripplanner.routing.services.TransitAlertService;
Expand Down Expand Up @@ -174,8 +176,19 @@ public DataFetcher<GraphQLTransitMode> mode() {

@Override
public DataFetcher<Iterable<TripPattern>> patterns() {
return environment ->
getTransitService(environment).getPatternsForRoute(getSource(environment));
return environment -> {
final TransitService transitService = getTransitService(environment);
var patterns = transitService.getPatternsForRoute(getSource(environment));

var args = new GraphQLTypes.GraphQLRoutePatternsArgs(environment.getArguments());

if (LocalDateRangeUtil.hasServiceDateFilter(args.getGraphQLServiceDates())) {
var filter = new PatternByServiceDatesFilter(args.getGraphQLServiceDates(), transitService);
return filter.filterPatterns(patterns);
} else {
return patterns;
}
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,35 @@ public void setGraphQLOriginModesWithParentStation(
}
}

public static class GraphQLLocalDateRangeInput {

private java.time.LocalDate end;
private java.time.LocalDate start;

public GraphQLLocalDateRangeInput(Map<String, Object> args) {
if (args != null) {
this.end = (java.time.LocalDate) args.get("end");
this.start = (java.time.LocalDate) args.get("start");
}
}

public java.time.LocalDate getGraphQLEnd() {
return this.end;
}

public java.time.LocalDate getGraphQLStart() {
return this.start;
}

public void setGraphQLEnd(java.time.LocalDate end) {
this.end = end;
}

public void setGraphQLStart(java.time.LocalDate start) {
this.start = start;
}
}

/** Identifies whether this stop represents a stop or station. */
public enum GraphQLLocationType {
ENTRANCE,
Expand Down Expand Up @@ -3449,13 +3478,16 @@ public static class GraphQLQueryTypeRoutesArgs {
private List<String> feeds;
private List<String> ids;
private String name;
private GraphQLLocalDateRangeInput serviceDates;
private List<GraphQLMode> transportModes;

public GraphQLQueryTypeRoutesArgs(Map<String, Object> args) {
if (args != null) {
this.feeds = (List<String>) args.get("feeds");
this.ids = (List<String>) args.get("ids");
this.name = (String) args.get("name");
this.serviceDates =
new GraphQLLocalDateRangeInput((Map<String, Object>) args.get("serviceDates"));
if (args.get("transportModes") != null) {
this.transportModes =
((List<Object>) args.get("transportModes")).stream()
Expand All @@ -3478,6 +3510,10 @@ public String getGraphQLName() {
return this.name;
}

public GraphQLLocalDateRangeInput getGraphQLServiceDates() {
return this.serviceDates;
}

public List<GraphQLMode> getGraphQLTransportModes() {
return this.transportModes;
}
Expand All @@ -3494,6 +3530,10 @@ public void setGraphQLName(String name) {
this.name = name;
}

public void setGraphQLServiceDates(GraphQLLocalDateRangeInput serviceDates) {
this.serviceDates = serviceDates;
}

public void setGraphQLTransportModes(List<GraphQLMode> transportModes) {
this.transportModes = transportModes;
}
Expand Down Expand Up @@ -3933,6 +3973,26 @@ public void setGraphQLLanguage(String language) {
}
}

public static class GraphQLRoutePatternsArgs {

private GraphQLLocalDateRangeInput serviceDates;

public GraphQLRoutePatternsArgs(Map<String, Object> args) {
if (args != null) {
this.serviceDates =
new GraphQLLocalDateRangeInput((Map<String, Object>) args.get("serviceDates"));
}
}

public GraphQLLocalDateRangeInput getGraphQLServiceDates() {
return this.serviceDates;
}

public void setGraphQLServiceDates(GraphQLLocalDateRangeInput serviceDates) {
this.serviceDates = serviceDates;
}
}

/** Entities that are relevant for routes that can contain alerts */
public enum GraphQLRouteAlertType {
AGENCY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ config:
Speed: Double
Reluctance: Double
Ratio: Double

LocalDate: java.time.LocalDate
mappers:
AbsoluteDirection: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLAbsoluteDirection#GraphQLAbsoluteDirection
Agency: org.opentripplanner.transit.model.organization.Agency#Agency
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.opentripplanner.apis.gtfs.model;

import java.time.LocalDate;
import javax.annotation.Nullable;

/**
* See the API documentation for a discussion of {@code startInclusive} and {@code endExclusive}.
*/
public record LocalDateRange(@Nullable LocalDate startInclusive, @Nullable LocalDate endExclusive) {
/**
* Does it actually define a limit or is the range unlimited?
*/
public boolean unlimited() {
return startInclusive == null && endExclusive == null;
}

/**
* Is the start date before the end (
*/
public boolean startBeforeEnd() {
return startInclusive != null && endExclusive != null && startInclusive.isAfter(endExclusive);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.opentripplanner.apis.gtfs.support.time;

import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
import org.opentripplanner.apis.gtfs.model.LocalDateRange;

public class LocalDateRangeUtil {

/**
* Checks if a service date filter input has at least one filter set. If both start and end are
* null then no filtering is necessary and this method returns null.
*/
public static boolean hasServiceDateFilter(GraphQLTypes.GraphQLLocalDateRangeInput dateRange) {
return (
dateRange != null &&
!new LocalDateRange(dateRange.getGraphQLStart(), dateRange.getGraphQLEnd()).unlimited()
);
}
}
Loading
Loading