Skip to content

Commit

Permalink
Add timePenalty to Transmodel API
Browse files Browse the repository at this point in the history
  • Loading branch information
t2gran committed Jun 14, 2024
1 parent b1a7c53 commit 56d4a0d
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ public ApiItinerary mapItinerary(Itinerary domain) {
api.transitTime = domain.getTransitDuration().toSeconds();
api.waitingTime = domain.getWaitingDuration().toSeconds();
api.walkDistance = domain.getNonTransitDistanceMeters();
api.generalizedCost = domain.getGeneralizedCost();
// We list only the generalizedCostIncludingPenalty, this is the least confusing. We intend to
// delete this endpoint soon, so we will not make the proper change and add the
// generalizedCostIncludingPenalty to the response and update the debug client to show it.
api.generalizedCost = domain.getGeneralizedCostIncludingPenalty();
api.elevationLost = domain.getElevationLost();
api.elevationGained = domain.getElevationGained();
api.transfers = domain.getNumberOfTransfers();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import org.opentripplanner.apis.transmodel.model.plan.PathGuidanceType;
import org.opentripplanner.apis.transmodel.model.plan.PlanPlaceType;
import org.opentripplanner.apis.transmodel.model.plan.RoutingErrorType;
import org.opentripplanner.apis.transmodel.model.plan.TripPatternTimePenaltyType;
import org.opentripplanner.apis.transmodel.model.plan.TripPatternType;
import org.opentripplanner.apis.transmodel.model.plan.TripQuery;
import org.opentripplanner.apis.transmodel.model.plan.TripType;
Expand Down Expand Up @@ -314,6 +315,7 @@ private GraphQLSchema create() {
gqlUtil
);

GraphQLObjectType tripPatternTimePenaltyType = TripPatternTimePenaltyType.create();
GraphQLObjectType tripMetadataType = TripMetadataType.create(gqlUtil);
GraphQLObjectType placeType = PlanPlaceType.create(
bikeRentalStationType,
Expand All @@ -339,7 +341,12 @@ private GraphQLSchema create() {
elevationStepType,
gqlUtil
);
GraphQLObjectType tripPatternType = TripPatternType.create(systemNoticeType, legType, gqlUtil);
GraphQLObjectType tripPatternType = TripPatternType.create(
systemNoticeType,
legType,
tripPatternTimePenaltyType,
gqlUtil
);
GraphQLObjectType routingErrorType = RoutingErrorType.create();

GraphQLOutputType tripType = TripType.create(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.opentripplanner.apis.transmodel.model.plan;

import graphql.Scalars;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import org.opentripplanner.framework.time.DurationUtils;

public class TripPatternTimePenaltyType {

public static GraphQLObjectType create() {
return GraphQLObjectType
.newObject()
.name("TimePenalty")
.description(
"""
The time-penalty is applied to either the access-legs and/or egress-legs. Both access and
egress may contain more than one leg; Hence, the penalty is not a field on leg.
Note! This is for debugging only. This type can change without notice.
"""
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
.name("appliedTo")
.description(
"""
The time-penalty is applied to either the access-legs and/or egress-legs. Both access
and egress may contain more than one leg; Hence, the penalty is not a field on leg. The
`appliedTo` describe witch part of the itinerary that this instance applies to.
"""
)
.type(Scalars.GraphQLString)
.dataFetcher(environment -> penalty(environment).appliesTo())
.build()
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
.name("timePenalty")
.description(
"""
The time-penalty added to the actual time/duration when comparing the itinerary with
other itineraries. This is used to decide witch is the best option, but is not visible
- the actual departure and arrival-times are not modified.
"""
)
.type(Scalars.GraphQLString)
.dataFetcher(environment ->
DurationUtils.durationToStr(penalty(environment).penalty().time())
)
.build()
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
.name("generalizedCostPenalty")
.description(
"""
The time-penalty does also propagate to the `generalizedCost` But, while the
arrival-/departure-times listed is not affected, the generalized-cost is. In some cases
the time-penalty-cost is excluded when comparing itineraries - that happens if one of
the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for
direct searches, so it needs to be excluded from such comparison to be fair. The unit
is transit-seconds.
"""
)
.type(Scalars.GraphQLInt)
.dataFetcher(environment -> penalty(environment).penalty().cost().toSeconds())
.build()
)
.build();
}

static TripPlanTimePenaltyDto penalty(DataFetchingEnvironment environment) {
return environment.getSource();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.opentripplanner.apis.transmodel.support.GqlUtil;
import org.opentripplanner.framework.model.TimeAndCost;
import org.opentripplanner.model.plan.Itinerary;

public class TripPatternType {

public static GraphQLObjectType create(
GraphQLOutputType systemNoticeType,
GraphQLObjectType legType,
GraphQLObjectType timePenaltyType,
GqlUtil gqlUtil
) {
return GraphQLObjectType
Expand Down Expand Up @@ -189,7 +194,7 @@ public static GraphQLObjectType create(
.name("generalizedCost")
.description("Generalized cost or weight of the itinerary. Used for debugging.")
.type(Scalars.GraphQLInt)
.dataFetcher(env -> itinerary(env).getGeneralizedCost())
.dataFetcher(env -> itinerary(env).getGeneralizedCostIncludingPenalty())
.build()
)
.field(
Expand Down Expand Up @@ -228,6 +233,23 @@ public static GraphQLObjectType create(
.dataFetcher(env -> itinerary(env).getTransferPriorityCost())
.build()
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
.name("timePenalty")
.description(
"""
A time and cost penalty applied to access and egress to favor regular scheduled
transit over potentially faster options with FLEX, Car, bike and scooter.
Note! This field is meant for debugging only. The field can be removed without notice
in the future.
"""
)
.type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(timePenaltyType))))
.dataFetcher(env -> TripPlanTimePenaltyDto.map(itinerary(env)))
.build()
)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.opentripplanner.apis.transmodel.model.plan;

import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import org.opentripplanner.framework.model.TimeAndCost;
import org.opentripplanner.model.plan.Itinerary;

/**
* A simple data-transfer-object used to map from an itinerary to the API specific
* type. It is needed because we need to pass in the "appliedTo" field, which does not
* exist in the domain model.
*/
public record TripPlanTimePenaltyDto(String appliesTo, TimeAndCost penalty) {
static List<TripPlanTimePenaltyDto> map(Itinerary itinerary) {
// This check for null to be robust - in case of a mistake in the future.
// The check is redundant on purpose.
if (itinerary == null) {
return List.of();
}
return Stream
.of(map("access", itinerary.getAccessPenalty()), map("egress", itinerary.getEgressPenalty()))
.filter(Objects::nonNull)
.toList();
}

static TripPlanTimePenaltyDto map(String appliedTo, TimeAndCost penalty) {
return penalty == null || penalty.isZero()
? null
: new TripPlanTimePenaltyDto(appliedTo, penalty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,36 @@ type TimeAndDayOffset {
time: Time
}

"""
The time-penalty is applied to either the access-legs and/or egress-legs. Both access and
egress may contain more than one leg; Hence, the penalty is not a field on leg.
Note! This is for debugging only. This type can change without notice.
"""
type TimePenalty {
"""
The time-penalty is applied to either the access-legs and/or egress-legs. Both access
and egress may contain more than one leg; Hence, the penalty is not a field on leg. The
`appliedTo` describe witch part of the itinerary that this instance applies to.
"""
appliedTo: String
"""
The time-penalty does also propagate to the `generalizedCost` But, while the
arrival-/departure-times listed is not affected, the generalized-cost is. In some cases
the time-penalty-cost is excluded when comparing itineraries - that happens if one of
the itineraries is a "direct/street-only" itinerary. Time-penalty can not be set for
direct searches, so it needs to be excluded from such comparison to be fair. The unit
is transit-seconds.
"""
generalizedCostPenalty: Int
"""
The time-penalty added to the actual time/duration when comparing the itinerary with
other itineraries. This is used to decide witch is the best option, but is not visible
- the actual departure and arrival-times are not modified.
"""
timePenalty: String
}

"Scheduled passing times. These are not affected by real time updates."
type TimetabledPassingTime {
"Scheduled time of arrival at quay"
Expand Down Expand Up @@ -1309,6 +1339,14 @@ type TripPattern {
streetDistance: Float
"Get all system notices."
systemNotices: [SystemNotice!]!
"""
A time and cost penalty applied to access and egress to favor regular scheduled
transit over potentially faster options with FLEX, Car, bike and scooter.
Note! This field is meant for debugging only. The field can be removed without notice
in the future.
"""
timePenalty: [TimePenalty!]!
"A cost calculated to favor transfer with higher priority. This field is meant for debugging only."
transferPriorityCost: Int
"A cost calculated to distribute wait-time and avoid very short transfers. This field is meant for debugging only."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.opentripplanner.apis.transmodel.model.plan;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.framework.model.TimeAndCost;
import org.opentripplanner.framework.time.DurationUtils;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Place;
import org.opentripplanner.model.plan.TestItineraryBuilder;
import org.opentripplanner.transit.model._data.TransitModelForTest;

class TripPlanTimePenaltyDtoTest {

private static final TimeAndCost PENALTY = new TimeAndCost(
DurationUtils.duration("20m30s"),
Cost.costOfSeconds(21)
);

private final TransitModelForTest testModel = TransitModelForTest.of();
private final Place placeA = Place.forStop(testModel.stop("A").build());
private final Place placeB = Place.forStop(testModel.stop("B").build());

@Test
void mapSingeEntry() {
assertNull(TripPlanTimePenaltyDto.map("access", null));
assertNull(TripPlanTimePenaltyDto.map("access", TimeAndCost.ZERO));
assertEquals(
new TripPlanTimePenaltyDto("access", PENALTY),
TripPlanTimePenaltyDto.map("access", PENALTY)
);
}

@Test
void mapItineraryWithNoPenalty() {
var i = itinerary();
assertEquals(List.of(), TripPlanTimePenaltyDto.map(null));
assertEquals(List.of(), TripPlanTimePenaltyDto.map(i));
}

@Test
void mapItineraryWithAccess() {
var i = itinerary();
i.setAccessPenalty(PENALTY);
assertEquals(
List.of(new TripPlanTimePenaltyDto("access", PENALTY)),
TripPlanTimePenaltyDto.map(i)
);
}

@Test
void mapItineraryWithEgress() {
var i = itinerary();
i.setEgressPenalty(PENALTY);
assertEquals(
List.of(new TripPlanTimePenaltyDto("egress", PENALTY)),
TripPlanTimePenaltyDto.map(i)
);
}

private Itinerary itinerary() {
return TestItineraryBuilder.newItinerary(placeA).drive(100, 200, placeB).build();
}
}

0 comments on commit 56d4a0d

Please sign in to comment.