Skip to content

Commit

Permalink
Merge pull request #5910 from entur/otp2_add_generalized-cost-includi…
Browse files Browse the repository at this point in the history
…ng-penalty

Add timePenalty to Transmodel API
  • Loading branch information
t2gran authored Jun 27, 2024
2 parents d176d5f + 7f5ba09 commit 15b49f9
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 4 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("TimePenaltyWithCost")
.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("generalizedCostDelta")
.description(
"""
The time-penalty does also propagate to the `generalizedCost`. As a result of the given
time-penalty, the generalized-cost also increased by the given amount. This delta is
included in the itinerary generalized-cost. In some cases the generalized-cost-delta 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 @@ -16,6 +16,7 @@ public class TripPatternType {
public static GraphQLObjectType create(
GraphQLOutputType systemNoticeType,
GraphQLObjectType legType,
GraphQLObjectType timePenaltyType,
GqlUtil gqlUtil
) {
return GraphQLObjectType
Expand Down Expand Up @@ -189,7 +190,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 +229,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.of(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 javax.annotation.Nullable;
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> of(Itinerary itinerary) {
return Stream
.of(of("access", itinerary.getAccessPenalty()), of("egress", itinerary.getEgressPenalty()))
.filter(Objects::nonNull)
.toList();
}

/**
* Package local to be unit-testable.
*/
@Nullable
static TripPlanTimePenaltyDto of(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 @@ -112,7 +112,7 @@ public static class Builder<E extends Enum<E>> {
private final TimeAndCostPenaltyForEnum<E> original;
private final EnumMap<E, TimeAndCostPenalty> values;

Builder(TimeAndCostPenaltyForEnum<E> original) {
private Builder(TimeAndCostPenaltyForEnum<E> original) {
this.original = original;
this.values = original.copyValues();
}
Expand Down
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 TimePenaltyWithCost {
"""
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`. As a result of the given
time-penalty, the generalized-cost also increased by the given amount. This delta is
included in the itinerary generalized-cost. In some cases the generalized-cost-delta 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.
"""
generalizedCostDelta: 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: [TimePenaltyWithCost!]!
"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,66 @@
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 testCreateFromSingeEntry() {
assertNull(TripPlanTimePenaltyDto.of("access", null));
assertNull(TripPlanTimePenaltyDto.of("access", TimeAndCost.ZERO));
assertEquals(
new TripPlanTimePenaltyDto("access", PENALTY),
TripPlanTimePenaltyDto.of("access", PENALTY)
);
}

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

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

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

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

0 comments on commit 15b49f9

Please sign in to comment.