Skip to content

Commit

Permalink
fix: Properly implement the relaxTransitGroupPriority parameter in …
Browse files Browse the repository at this point in the history
…Transmodel API
  • Loading branch information
t2gran committed Dec 21, 2023
1 parent 3e46beb commit 45c7fba
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ private BoardAndAlightSlack mapTransit() {
setIfNotNull(req.ignoreRealtimeUpdates, tr::setIgnoreRealtimeUpdates);

if (req.relaxTransitGroupPriority != null) {
tr.withTransitGroupPriorityGeneralizedCostSlack(
CostLinearFunction.of(req.relaxTransitGroupPriority)
);
tr.withRelaxTransitGroupPriority(CostLinearFunction.of(req.relaxTransitGroupPriority));
} else {
setIfNotNull(
req.relaxTransitSearchGeneralizedCostAtDestination,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.opentripplanner.apis.transmodel.mapping.preferences;

import graphql.schema.DataFetchingEnvironment;
import java.util.Map;
import org.opentripplanner.apis.transmodel.model.TransportModeSlack;
import org.opentripplanner.apis.transmodel.model.plan.RelaxCostType;
import org.opentripplanner.apis.transmodel.support.DataFetcherDecorator;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;
import org.opentripplanner.routing.api.request.preference.TransitPreferences;

public class TransitPreferencesMapper {
Expand Down Expand Up @@ -35,7 +38,10 @@ public static void mapTransitPreferences(
callWith.argument("includeRealtimeCancellations", transit::setIncludeRealtimeCancellations);
callWith.argument(
"relaxTransitGroupPriority",
transit::withTransitGroupPriorityGeneralizedCostSlack
it ->
transit.withRelaxTransitGroupPriority(
RelaxCostType.mapToDomain((Map<String, Object>) it, CostLinearFunction.NORMAL)
)
);
callWith.argument(
"relaxTransitSearchGeneralizedCostAtDestination",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import graphql.Scalars;
import graphql.language.FloatValue;
import graphql.language.IntValue;
import graphql.language.ObjectField;
import graphql.language.ObjectValue;
import graphql.language.StringValue;
import graphql.schema.GraphQLInputObjectField;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLNonNull;
import java.util.Map;
import org.opentripplanner.framework.graphql.scalar.CostScalarFactory;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.framework.time.DurationUtils;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;

public class RelaxCostType {
Expand All @@ -26,8 +28,8 @@ public class RelaxCostType {
with twice as high cost as another one, is accepted. A `constant=$300` means a "fixed"
constant is added to the limit. A `{ratio=1.0, constant=0}` is said to be the NORMAL relaxed
cost - the limit is the same as the cost used to calculate the limit. The NORMAL is usually
the default. We can express the RelaxCost as a function `f(x) = constant + ratio * x`.
`f(x)=x` is the NORMAL function.
the default. We can express the RelaxCost as a function `f(t) = constant + ratio * t`.
`f(t)=t` is the NORMAL function.
"""
)
.field(
Expand All @@ -44,11 +46,12 @@ public class RelaxCostType {
.newInputObjectField()
.name(CONSTANT)
.description(
"The constant value to add to the limit. Must be a positive number. The unit" +
" is cost-seconds."
"The constant value to add to the limit. Must be a positive number. The value is" +
"equivalent to transit-cost-seconds. Integers is treated as seconds, but you may use " +
"the duration format. Example: '3665 = 'DT1h1m5s' = '1h1m5s'."
)
.defaultValueLiteral(IntValue.of(0))
.type(new GraphQLList(new GraphQLNonNull(Scalars.GraphQLID)))
.defaultValueProgrammatic("0s")
.type(CostScalarFactory.costScalar())
.build()
)
.build();
Expand All @@ -63,9 +66,31 @@ public static ObjectValue valueOf(CostLinearFunction value) {
ObjectField
.newObjectField()
.name(CONSTANT)
.value(IntValue.of(value.constant().toSeconds()))
// We only use this to display default value (this is an input type), so using the
// lenient OTP version of duration is ok - it is slightly more readable.
.value(StringValue.of(DurationUtils.durationToStr(value.constant().asDuration())))
.build()
)
.build();
}

public static CostLinearFunction mapToDomain(
Map<String, Object> input,
CostLinearFunction defaultValue
) {
if (input == null || input.isEmpty()) {
return defaultValue;
}

double ratio = 1.0;
Cost constant = Cost.ZERO;

if (input.containsKey(RATIO)) {
ratio = (Double) input.get(RATIO);
}
if (input.containsKey(CONSTANT)) {
constant = (Cost) input.get(CONSTANT);
}
return CostLinearFunction.of(constant, ratio);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ public static GraphQLFieldDefinition create(
itinerary-filters.
- The `ratio` must be greater or equal to 1.0 and less then 1.2.
- The `slack` must be greater or equal to 0 and less then 3600.
- The `constant` must be greater or equal to '0s' and less then '1h'.
THIS IS STILL AN EXPERIMENTAL FEATURE - IT MAY CHANGE WITHOUT ANY NOTICE!
""".stripIndent()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.opentripplanner.framework.graphql.scalar;

import graphql.GraphQLContext;
import graphql.execution.CoercedVariables;
import graphql.language.StringValue;
import graphql.language.Value;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.GraphQLScalarType;
import java.util.Locale;
import java.util.NoSuchElementException;
import javax.annotation.Nonnull;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.framework.time.DurationUtils;

public class CostScalarFactory {

private static final String TYPENAME = "Cost";

private static final String DOCUMENTATION =
"A cost value, normally a value of 1 is equivalent to riding transit for 1 second, " +
"but it might not depending on the use-case. Format: 3665 = DT1h1m5s = 1h1m5s";

private static final GraphQLScalarType SCALAR_INSTANCE = createCostScalar();

private CostScalarFactory() {}

public static GraphQLScalarType costScalar() {
return SCALAR_INSTANCE;
}

private static GraphQLScalarType createCostScalar() {
return GraphQLScalarType
.newScalar()
.name(TYPENAME)
.description(DOCUMENTATION)
.coercing(createCoercing())
.build();
}

private static String serializeCost(Cost cost) {
return cost.asDuration().toString();
}

private static Cost parseCost(String input) throws CoercingParseValueException {
try {
return Cost.fromDuration(DurationUtils.parseSecondsOrDuration(input).orElseThrow());
} catch (IllegalArgumentException | NoSuchElementException e) {
throw new CoercingParseValueException(e.getMessage(), e);
}
}

private static Coercing<Cost, String> createCoercing() {
return new Coercing<>() {
@Override
public String serialize(@Nonnull Object result, GraphQLContext c, Locale l) {
return serializeCost((Cost) result);
}

@Override
public Cost parseValue(Object input, GraphQLContext c, Locale l)
throws CoercingParseValueException {
return parseCost((String) input);
}

@Override
public Cost parseLiteral(Value<?> input, CoercedVariables v, GraphQLContext c, Locale l)
throws CoercingParseLiteralException {
if (input instanceof StringValue stringValue) {
return parseCost(stringValue.getValue());
}
return null;
}

@Override
@Nonnull
public Value<?> valueToLiteral(Object input, GraphQLContext c, Locale l) {
return StringValue.of((String) input);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ public Builder setUnpreferredCostString(String constFunction) {
return setUnpreferredCost(CostLinearFunction.of(constFunction));
}

public Builder withTransitGroupPriorityGeneralizedCostSlack(CostLinearFunction value) {
public Builder withRelaxTransitGroupPriority(CostLinearFunction value) {
this.relaxTransitGroupPriority = value;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,7 @@ The board time is added to the time when going from the stop (offboard) to onboa
.asString(dft.relaxTransitGroupPriority().toString());

if (relaxTransitGroupPriorityValue != null) {
builder.withTransitGroupPriorityGeneralizedCostSlack(
CostLinearFunction.of(relaxTransitGroupPriorityValue)
);
builder.withRelaxTransitGroupPriority(CostLinearFunction.of(relaxTransitGroupPriorityValue));
}

// TODO REMOVE THIS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ type QueryType {
itinerary-filters.
- The `ratio` must be greater or equal to 1.0 and less then 1.2.
- The `slack` must be greater or equal to 0 and less then 3600.
- The `constant` must be greater or equal to '0s' and less then '1h'.
THIS IS STILL AN EXPERIMENTAL FEATURE - IT MAY CHANGE WITHOUT ANY NOTICE!
"""
Expand Down Expand Up @@ -1286,7 +1286,7 @@ type TripPattern {
expectedStartTime: DateTime!
"Generalized cost or weight of the itinerary. Used for debugging."
generalizedCost: Int
"A second cost or weight of the itinerary. Some use-cases like pass-through and transit-priority-groups uses a second cost during routing. This is used for debugging."
"A second cost or weight of the itinerary. Some use-cases like pass-through and transit-priority-groups use a second cost during routing. This is used for debugging."
generalizedCost2: Int
"A list of legs. Each leg is either a walking (cycling, car) portion of the trip, or a ride leg on a particular vehicle. So a trip where the use walks to the Q train, transfers to the 6, then walks to their destination, has four legs."
legs: [Leg!]!
Expand Down Expand Up @@ -1867,6 +1867,9 @@ enum WheelchairBoarding {
"List of coordinates like: [[60.89, 11.12], [62.56, 12.10]]"
scalar Coordinates

"A cost value, normally a value of 1 is equivalent to riding transit for 1 second, but it might not depending on the use-case. Format: 3665 = DT1h1m5s = 1h1m5s"
scalar Cost

"Local date using the ISO 8601 format: `YYYY-MM-DD`. Example: `2020-05-17`."
scalar Date

Expand Down Expand Up @@ -2028,12 +2031,12 @@ This is used to include more results into the result. A `ratio=2.0` means a path
with twice as high cost as another one, is accepted. A `constant=$300` means a "fixed"
constant is added to the limit. A `{ratio=1.0, constant=0}` is said to be the NORMAL relaxed
cost - the limit is the same as the cost used to calculate the limit. The NORMAL is usually
the default. We can express the RelaxCost as a function `f(x) = constant + ratio * x`.
`f(x)=x` is the NORMAL function.
the default. We can express the RelaxCost as a function `f(t) = constant + ratio * t`.
`f(t)=t` is the NORMAL function.
"""
input RelaxCostInput {
"The constant value to add to the limit. Must be a positive number. The unit is cost-seconds."
constant: [ID!] = 0
"The constant value to add to the limit. Must be a positive number. The value isequivalent to transit-cost-seconds. Integers is treated as seconds, but you may use the duration format. Example: '3665 = 'DT1h1m5s' = '1h1m5s'."
constant: Cost = "0s"
"The factor to multiply with the 'other cost'. Minimum value is 1.0."
ratio: Float = 1.0
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.opentripplanner.apis.transmodel.model.plan;

import static org.junit.jupiter.api.Assertions.*;
import static org.opentripplanner.apis.transmodel.model.plan.RelaxCostType.CONSTANT;
import static org.opentripplanner.apis.transmodel.model.plan.RelaxCostType.RATIO;

import graphql.language.FloatValue;
import graphql.language.ObjectField;
import graphql.language.ObjectValue;
import graphql.language.StringValue;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;

class RelaxCostTypeTest {

@Test
void valueOf() {
assertEquals(
ObjectValue
.newObjectValue()
.objectField(ObjectField.newObjectField().name(RATIO).value(FloatValue.of(1.0)).build())
.objectField(
ObjectField.newObjectField().name(CONSTANT).value(StringValue.of("0s")).build()
)
.build()
.toString(),
RelaxCostType.valueOf(CostLinearFunction.NORMAL).toString()
);
assertEquals(
ObjectValue
.newObjectValue()
.objectField(ObjectField.newObjectField().name(RATIO).value(FloatValue.of(1.3)).build())
.objectField(
ObjectField.newObjectField().name(CONSTANT).value(StringValue.of("1m7s")).build()
)
.build()
.toString(),
RelaxCostType.valueOf(CostLinearFunction.of(Cost.costOfSeconds(67), 1.3)).toString()
);
}

@Test
void mapToDomain() {
Map<String, Object> input;

input = Map.of(RATIO, 1.0, CONSTANT, Cost.ZERO);
assertEquals(
CostLinearFunction.NORMAL,
RelaxCostType.mapToDomain(input, CostLinearFunction.ZERO)
);

input = Map.of(RATIO, 0.0, CONSTANT, Cost.ZERO);
assertEquals(
CostLinearFunction.ZERO,
RelaxCostType.mapToDomain(input, CostLinearFunction.ZERO)
);

input = Map.of(RATIO, 1.7, CONSTANT, Cost.costOfSeconds(3600 + 3 * 60 + 7));
assertEquals(
CostLinearFunction.of("1h3m7s + 1.7t"),
RelaxCostType.mapToDomain(input, CostLinearFunction.ZERO)
);
assertEquals(
CostLinearFunction.NORMAL,
RelaxCostType.mapToDomain(null, CostLinearFunction.NORMAL)
);
assertEquals(CostLinearFunction.ZERO, RelaxCostType.mapToDomain(null, CostLinearFunction.ZERO));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class TransitPreferencesTest {
.setUnpreferredCost(UNPREFERRED_COST)
.withBoardSlack(b -> b.withDefault(D45s).with(TransitMode.AIRPLANE, D35m))
.withAlightSlack(b -> b.withDefault(D15s).with(TransitMode.AIRPLANE, D25m))
.withTransitGroupPriorityGeneralizedCostSlack(TRANSIT_GROUP_PRIORITY_RELAX)
.withRelaxTransitGroupPriority(TRANSIT_GROUP_PRIORITY_RELAX)
.setIgnoreRealtimeUpdates(IGNORE_REALTIME_UPDATES)
.setIncludePlannedCancellations(INCLUDE_PLANNED_CANCELLATIONS)
.setIncludeRealtimeCancellations(INCLUDE_REALTIME_CANCELLATIONS)
Expand Down

0 comments on commit 45c7fba

Please sign in to comment.