Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,28 +1,39 @@
package org.opentripplanner.ext.transmodelapi.mapping;

import static graphql.execution.ExecutionContextBuilder.newExecutionContextBuilder;
import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.opentripplanner.framework.time.TimeUtils.time;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;

import graphql.ExecutionInput;
import graphql.execution.ExecutionId;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.DataFetchingEnvironmentImpl;
import io.micrometer.core.instrument.Metrics;
import java.time.Duration;
import java.util.Arrays;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.transmodelapi.TransmodelRequestContext;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.model.calendar.CalendarServiceData;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.Place;
import org.opentripplanner.model.plan.PlanTestConstants;
import org.opentripplanner.model.plan.ScheduledTransitLeg;
import org.opentripplanner.raptor.configure.RaptorConfig;
import org.opentripplanner.routing.api.request.PassThroughPoint;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.StreetMode;
import org.opentripplanner.routing.api.request.preference.StreetPreferences;
Expand All @@ -36,20 +47,58 @@
import org.opentripplanner.standalone.config.RouterConfig;
import org.opentripplanner.standalone.server.DefaultServerRequestContext;
import org.opentripplanner.test.support.VariableSource;
import org.opentripplanner.transit.model._data.TransitModelForTest;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.network.Route;
import org.opentripplanner.transit.model.network.TripPattern;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.service.DefaultTransitService;
import org.opentripplanner.transit.service.StopModel;
import org.opentripplanner.transit.service.TransitModel;

public class TripRequestMapperTest implements PlanTestConstants {

static final TransmodelRequestContext context;
private static final Duration MAX_FLEXIBLE = Duration.ofMinutes(20);

private static final Function<StopLocation, String> STOP_TO_ID = s -> s.getId().toString();

private static final Route route1 = TransitModelForTest.route("route1").build();
private static final Route route2 = TransitModelForTest.route("route2").build();

private static final RegularStop stop1 = TransitModelForTest.stopForTest("ST:stop1", 1, 1);
private static final RegularStop stop2 = TransitModelForTest.stopForTest("ST:stop2", 2, 1);
private static final RegularStop stop3 = TransitModelForTest.stopForTest("ST:stop3", 3, 1);

static {
var graph = new Graph();
var transitModel = new TransitModel();
var itinerary = newItinerary(Place.forStop(stop1), time("11:00"))
.bus(route1, 1, time("11:05"), time("11:20"), Place.forStop(stop2))
.bus(route2, 2, time("11:20"), time("11:40"), Place.forStop(stop3))
.build();
var patterns = itineraryPatterns(itinerary);
var stopModel = StopModel
.of()
.withRegularStop(stop1)
.withRegularStop(stop2)
.withRegularStop(stop3)
.build();

var transitModel = new TransitModel(stopModel, new Deduplicator());
transitModel.initTimeZone(ZoneIds.STOCKHOLM);
var calendarServiceData = new CalendarServiceData();
LocalDate serviceDate = itinerary.startTime().toLocalDate();
patterns.forEach(pattern -> {
transitModel.addTripPattern(pattern.getId(), pattern);
final int serviceCode = pattern.getScheduledTimetable().getTripTimes(0).getServiceCode();
transitModel.getServiceCodes().put(pattern.getId(), serviceCode);
calendarServiceData.putServiceDatesForServiceId(pattern.getId(), List.of(serviceDate));
});

transitModel.updateCalendarServiceData(true, calendarServiceData, DataImportIssueStore.NOOP);
transitModel.index();
final var transitService = new DefaultTransitService(transitModel);

var defaultRequest = new RouteRequest();

// Change defaults for FLEXIBLE to a lower value than the default 45m. This should restrict the
Expand Down Expand Up @@ -243,6 +292,42 @@ public void testBikeTriangleFactorsHasNoEffect(BicycleOptimizeType bot) {
assertEquals(TimeSlopeSafetyTriangle.DEFAULT, req1.preferences().bike().optimizeTriangle());
}

@Test
void testPassThroughPoints() {
TransitIdMapper.clearFixedFeedId();

final List<String> PTP1 = List.of(stop1, stop2, stop3).stream().map(STOP_TO_ID).toList();
final List<String> PTP2 = List.of(stop2, stop3, stop1).stream().map(STOP_TO_ID).toList();
final Map<String, Object> arguments = Map.of(
"passThroughPoints",
List.of(Map.of("name", "PTP1", "placeIds", PTP1), Map.of("placeIds", PTP2, "name", "PTP2"))
);

final List<PassThroughPoint> points = TripRequestMapper
.createRequest(executionContext(arguments))
.getPassThroughPoints();
assertEquals(PTP1, points.get(0).stopLocations().stream().map(STOP_TO_ID).toList());
assertEquals("PTP1", points.get(0).name());
assertEquals(PTP2, points.get(1).stopLocations().stream().map(STOP_TO_ID).toList());
assertEquals("PTP2", points.get(1).name());
}

@Test
void testPassThroughPointsNoMatch() {
TransitIdMapper.clearFixedFeedId();

final Map<String, Object> arguments = Map.of(
"passThroughPoints",
List.of(Map.of("placeIds", List.of("F:XX:NonExisting")))
);

final RuntimeException ex = assertThrows(
RuntimeException.class,
() -> TripRequestMapper.createRequest(executionContext(arguments))
);
assertEquals("No match for F:XX:NonExisting.", ex.getMessage());
}

private DataFetchingEnvironment executionContext(Map<String, Object> arguments) {
ExecutionInput executionInput = ExecutionInput
.newExecutionInput()
Expand All @@ -265,4 +350,14 @@ private DataFetchingEnvironment executionContext(Map<String, Object> arguments)

return env;
}

private static List<TripPattern> itineraryPatterns(final Itinerary itinerary) {
return itinerary
.getLegs()
.stream()
.filter(Leg::isScheduledTransitLeg)
.map(Leg::asScheduledTransitLeg)
.map(ScheduledTransitLeg::getTripPattern)
.collect(toList());
}
}
16 changes: 15 additions & 1 deletion src/ext/graphql/transmodelapi/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,8 @@ type QueryType {
numTripPatterns: Int = 50,
"Use the cursor to go to the next \"page\" of itineraries. Copy the cursor from the last response and keep the original request as is. This will enable you to search for itineraries in the next or previous time-window."
pageCursor: String,
"The list of points the journey is required to pass through."
passThroughPoints: [PassThroughPoint!],
"""
Whether non-optimal transit paths at the destination should be returned. Let c be the
existing minimum pareto optimal generalized-cost to beat. Then a trip with cost c' is
Expand Down Expand Up @@ -1935,6 +1937,18 @@ input Modes {
transportModes: [TransportModes]
}

"Defines one point which the journey must pass through."
input PassThroughPoint {
"Optional name of the pass-through point for debugging and logging. It is not used in routing."
name: String
"""
The list of *stop location ids* which define the pass-through point. At least one id is required.
Quay, StopPlace, multimodal StopPlace, and GroupOfStopPlaces are supported location types.
The journey must pass through at least one of these entities - not all of them.
"""
placeIds: [String!]
}

"A combination of street mode and penalty for time and cost."
input PenaltyForStreetMode {
"""
Expand Down Expand Up @@ -2028,7 +2042,7 @@ input ViaLocationInput {
maxSlack: Duration = "PT1H"
"The minimum time the user wants to stay in the via location before continuing his journey"
minSlack: Duration = "PT5M"
"The name of the location. This is pass-through informationand is not used in routing."
"The name of the location. This is pass-through information and is not used in routing."
name: String
"The id of an element in the OTP model. Currently supports Quay, StopPlace, multimodal StopPlace, and GroupOfStopPlaces."
place: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.opentripplanner.ext.transmodelapi.mapping;

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.opentripplanner.routing.api.request.PassThroughPoint;
import org.opentripplanner.transit.service.TransitService;

class PassThroughLocationMapper {

static List<PassThroughPoint> toLocations(
final TransitService transitService,
final List<Map<String, Object>> passThroughPoints
) {
return passThroughPoints
.stream()
.map(p -> handlePoint(transitService, p))
.filter(Objects::nonNull)
.collect(toList());
// TODO Propagate an error if a stopplace is unknown and fails lookup.
}

private static PassThroughPoint handlePoint(
final TransitService transitService,
Map<String, Object> map
) {
final List<String> stops = (List<String>) map.get("placeIds");
final String name = (String) map.get("name");
if (stops == null) {
return null;
}

return stops
.stream()
.map(TransitIdMapper::mapIDToDomain)
.flatMap(id -> {
var stopLocations = transitService.getStopOrChildStops(id);
if (stopLocations.isEmpty()) {
throw new RuntimeException("No match for %s.".formatted(id));
}
return stopLocations.stream();
})
.collect(collectingAndThen(toList(), sls -> new PassThroughPoint(sls, name)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,15 @@ public static String setupFixedFeedId(Collection<? extends AbstractTransitEntity
);
return fixedFeedId;
}

/**
* Clear the globally configured feed id.
* <p>
* For use from tests only.
*
* @see #setupFixedFeedId(Collection)
*/
public static void clearFixedFeedId() {
fixedFeedId = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.standalone.api.OtpServerRequestContext;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.service.TransitService;

public class TripRequestMapper {

Expand All @@ -40,6 +41,13 @@ public static RouteRequest createRequest(DataFetchingEnvironment environment) {
"to",
(Map<String, Object> v) -> request.setTo(GenericLocationMapper.toGenericLocation(v))
);
final TransitService transitService = context.getTransitService();
callWith.argument(
"passThroughPoints",
(List<Map<String, Object>> v) -> {
request.setPassThroughPoints(PassThroughLocationMapper.toLocations(transitService, v));
}
);

callWith.argument(
"dateTime",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.opentripplanner.ext.transmodelapi.model.framework;

import graphql.Scalars;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLNonNull;

public class PassThroughPointInputType {

public static final GraphQLInputObjectType INPUT_TYPE = GraphQLInputObjectType
.newInputObject()
.name("PassThroughPoint")
.description("Defines one point which the journey must pass through.")
.field(
GraphQLInputObjectField
.newInputObjectField()
.name("name")
.description(
"Optional name of the pass-through point for debugging and logging. It is not used in routing."
)
.type(Scalars.GraphQLString)
.build()
)
.field(
GraphQLInputObjectField
.newInputObjectField()
.name("placeIds")
.description(
"""
The list of *stop location ids* which define the pass-through point. At least one id is required.
Quay, StopPlace, multimodal StopPlace, and GroupOfStopPlaces are supported location types.
The journey must pass through at least one of these entities - not all of them."""
)
.type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLString)))
.build()
)
.build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.opentripplanner.ext.transmodelapi.model.EnumTypes;
import org.opentripplanner.ext.transmodelapi.model.TransportModeSlack;
import org.opentripplanner.ext.transmodelapi.model.framework.LocationInputType;
import org.opentripplanner.ext.transmodelapi.model.framework.PassThroughPointInputType;
import org.opentripplanner.ext.transmodelapi.model.framework.PenaltyForStreetModeType;
import org.opentripplanner.ext.transmodelapi.support.GqlUtil;
import org.opentripplanner.routing.api.request.preference.RoutingPreferences;
Expand Down Expand Up @@ -150,6 +151,14 @@ public static GraphQLFieldDefinition create(
.type(new GraphQLNonNull(LocationInputType.INPUT_TYPE))
.build()
)
.argument(
GraphQLArgument
.newArgument()
.name("passThroughPoints")
.description("The list of points the journey is required to pass through.")
.type(new GraphQLList(new GraphQLNonNull(PassThroughPointInputType.INPUT_TYPE)))
.build()
)
.argument(
GraphQLArgument
.newArgument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static GraphQLInputObjectType create(GqlUtil gqlUtil) {
.name("name")
.description(
"The name of the location. This is pass-through information" +
"and is not used in routing."
" and is not used in routing."
)
.type(Scalars.GraphQLString)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
import java.util.BitSet;
import java.util.Objects;
import java.util.stream.IntStream;
import javax.annotation.Nullable;

/**
* A collection of stop indexes used to define a pass through-point.
*/
public class PassThroughPoint {

private final int[] stops;
private final String name;

public PassThroughPoint(int[] stops) {
public PassThroughPoint(int[] stops, @Nullable String name) {
Objects.requireNonNull(stops);
if (stops.length == 0) {
throw new IllegalArgumentException("At least one stop is required");
}
this.stops = Arrays.copyOf(stops, stops.length);
this.name = name;
}

/**
Expand All @@ -43,6 +46,8 @@ public int hashCode() {

@Override
public String toString() {
return "(stops: " + Arrays.toString(stops) + ")";
return (
(name == null ? "(" : "(name: '" + name + "', ") + "stops: " + Arrays.toString(stops) + ")"
);
}
}
Loading