From 17c1d5b0e5523be8bbe967658ab60369bc0558da Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Fri, 14 Jun 2024 15:54:26 +0200 Subject: [PATCH 1/4] Add timePenalty to Transmodel API --- .../ext/restapi/mapping/ItineraryMapper.java | 5 +- .../transmodel/TransmodelGraphQLSchema.java | 9 ++- .../plan/TripPatternTimePenaltyType.java | 79 +++++++++++++++++++ .../model/plan/TripPatternType.java | 20 ++++- .../model/plan/TripPlanTimePenaltyDto.java | 32 ++++++++ .../apis/transmodel/schema.graphql | 38 +++++++++ .../plan/TripPlanTimePenaltyDtoTest.java | 67 ++++++++++++++++ 7 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java create mode 100644 src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java create mode 100644 src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java index d87cf33d71c..720d02fdd44 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/ItineraryMapper.java @@ -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(); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index 638d7783e9a..a88c36ac039 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -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; @@ -314,6 +315,7 @@ private GraphQLSchema create() { gqlUtil ); + GraphQLObjectType tripPatternTimePenaltyType = TripPatternTimePenaltyType.create(); GraphQLObjectType tripMetadataType = TripMetadataType.create(gqlUtil); GraphQLObjectType placeType = PlanPlaceType.create( bikeRentalStationType, @@ -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( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java new file mode 100644 index 00000000000..1a6e7310697 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java @@ -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(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java index 2238a39c139..c903016b91b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternType.java @@ -16,6 +16,7 @@ public class TripPatternType { public static GraphQLObjectType create( GraphQLOutputType systemNoticeType, GraphQLObjectType legType, + GraphQLObjectType timePenaltyType, GqlUtil gqlUtil ) { return GraphQLObjectType @@ -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( @@ -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(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java new file mode 100644 index 00000000000..b834b711327 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java @@ -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 of(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(of("access", itinerary.getAccessPenalty()), of("egress", itinerary.getEgressPenalty())) + .filter(Objects::nonNull) + .toList(); + } + + static TripPlanTimePenaltyDto of(String appliedTo, TimeAndCost penalty) { + return penalty == null || penalty.isZero() + ? null + : new TripPlanTimePenaltyDto(appliedTo, penalty); + } +} diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index fbaa2418d7e..b521a5f6ff0 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -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" @@ -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." diff --git a/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java new file mode 100644 index 00000000000..9ea6016324b --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java @@ -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 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(null)); + 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(); + } +} From 52c2c555b39b063440b78f59eee4d7182f628c0a Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 19 Jun 2024 17:09:36 +0200 Subject: [PATCH 2/4] Apply code review feedback --- .../transmodel/model/plan/TripPlanTimePenaltyDto.java | 10 +++++----- .../request/framework/TimeAndCostPenaltyForEnum.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java index b834b711327..9217ed5b437 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java @@ -3,6 +3,7 @@ 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; @@ -13,17 +14,16 @@ */ public record TripPlanTimePenaltyDto(String appliesTo, TimeAndCost penalty) { static List of(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(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 diff --git a/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java b/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java index c45fc3e33a8..2f439801cb5 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java +++ b/src/main/java/org/opentripplanner/routing/api/request/framework/TimeAndCostPenaltyForEnum.java @@ -112,7 +112,7 @@ public static class Builder> { private final TimeAndCostPenaltyForEnum original; private final EnumMap values; - Builder(TimeAndCostPenaltyForEnum original) { + private Builder(TimeAndCostPenaltyForEnum original) { this.original = original; this.values = original.copyValues(); } From 94c19f5f135647db2db6a0dbd7cb224d8ea2899c Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Wed, 19 Jun 2024 17:20:17 +0200 Subject: [PATCH 3/4] Code review: Cleanup naming in Transmodel API --- .../model/plan/TripPatternTimePenaltyType.java | 18 +++++++++--------- .../model/plan/TripPlanTimePenaltyDto.java | 6 +++--- .../apis/transmodel/schema.graphql | 18 +++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java index 1a6e7310697..744113929c7 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPatternTimePenaltyType.java @@ -11,11 +11,11 @@ public class TripPatternTimePenaltyType { public static GraphQLObjectType create() { return GraphQLObjectType .newObject() - .name("TimePenalty") + .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. + 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. """ @@ -55,15 +55,15 @@ public static GraphQLObjectType create() { .field( GraphQLFieldDefinition .newFieldDefinition() - .name("generalizedCostPenalty") + .name("generalizedCostDelta") .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. + 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) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java index 9217ed5b437..0e83384157f 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDto.java @@ -20,9 +20,9 @@ static List of(Itinerary itinerary) { .toList(); } - /** - * Package local to be unit-testable. - */ + /** + * Package local to be unit-testable. + */ @Nullable static TripPlanTimePenaltyDto of(String appliedTo, TimeAndCost penalty) { return penalty == null || penalty.isZero() diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index b521a5f6ff0..5c1912df4e6 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -1221,7 +1221,7 @@ 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 { +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 @@ -1229,14 +1229,14 @@ type TimePenalty { """ 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. + 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. """ - generalizedCostPenalty: Int + 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 @@ -1346,7 +1346,7 @@ type TripPattern { Note! This field is meant for debugging only. The field can be removed without notice in the future. """ - timePenalty: [TimePenalty!]! + 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." From 7f5ba09d13d261eeda69d3c799f9162d7570ea0e Mon Sep 17 00:00:00 2001 From: Thomas Gran Date: Thu, 20 Jun 2024 10:05:48 +0200 Subject: [PATCH 4/4] test: Fix broken unit-test TripPlanTimePenaltyDtoTest --- .../apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java index 9ea6016324b..579f9b73761 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/model/plan/TripPlanTimePenaltyDtoTest.java @@ -37,7 +37,6 @@ void testCreateFromSingeEntry() { @Test void testCreateFromItineraryWithNoPenalty() { var i = itinerary(); - assertEquals(List.of(), TripPlanTimePenaltyDto.of(null)); assertEquals(List.of(), TripPlanTimePenaltyDto.of(i)); }