Skip to content

Commit

Permalink
Merge remote-tracking branch 'entur/otp2_fix_transmodel_api_relax_tra…
Browse files Browse the repository at this point in the history
…nsit_group_priority' into otp2_entur_develop
  • Loading branch information
t2gran committed Dec 21, 2023
2 parents 464a0bc + 45c7fba commit fc6d1cd
Show file tree
Hide file tree
Showing 54 changed files with 1,279 additions and 859 deletions.
24 changes: 11 additions & 13 deletions docs/RouteRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe
| numItineraries | `integer` | The maximum number of itineraries to return. | *Optional* | `50` | 2.0 |
| [optimize](#rd_optimize) | `enum` | The set of characteristics that the user wants to optimize for. | *Optional* | `"safe"` | 2.0 |
| [otherThanPreferredRoutesPenalty](#rd_otherThanPreferredRoutesPenalty) | `integer` | Penalty added for using every route that is not preferred if user set any route as preferred. | *Optional* | `300` | 2.0 |
| [relaxTransitPriorityGroup](#rd_relaxTransitPriorityGroup) | `string` | The relax function for transit-priority-groups | *Optional* | `"0s + 1.00 t"` | 2.5 |
| [relaxTransitGroupPriority](#rd_relaxTransitGroupPriority) | `string` | The relax function for transit-group-priority | *Optional* | `"0s + 1.00 t"` | 2.5 |
| [relaxTransitSearchGeneralizedCostAtDestination](#rd_relaxTransitSearchGeneralizedCostAtDestination) | `double` | Whether non-optimal transit paths at the destination should be returned | *Optional* | | 2.3 |
| [searchWindow](#rd_searchWindow) | `duration` | The duration of the search-window. | *Optional* | | 2.0 |
| stairsReluctance | `double` | Used instead of walkReluctance for stairs. | *Optional* | `2.0` | 2.0 |
Expand Down Expand Up @@ -109,7 +109,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe
|    [extraStopBoardAlightCostsFactor](#rd_to_extraStopBoardAlightCostsFactor) | `double` | Add an extra board- and alight-cost for prioritized stops. | *Optional* | `0.0` | 2.1 |
|    [minSafeWaitTimeFactor](#rd_to_minSafeWaitTimeFactor) | `double` | Used to set a maximum wait-time cost, base on min-safe-transfer-time. | *Optional* | `5.0` | 2.1 |
|    [optimizeTransferWaitTime](#rd_to_optimizeTransferWaitTime) | `boolean` | This enables the transfer wait time optimization. | *Optional* | `true` | 2.1 |
| [transitPriorityGroups](#rd_transitPriorityGroups) | `object` | Transit priority groups configuration | *Optional* | | 2.5 |
| [transitGroupPriority](#rd_transitGroupPriority) | `object` | Group transit patterns and give each group a mutual advantage in the Raptor search. | *Optional* | | 2.5 |
| [transitReluctanceForMode](#rd_transitReluctanceForMode) | `enum map of double` | Transit reluctance for a given transport mode | *Optional* | | 2.1 |
| [unpreferred](#rd_unpreferred) | `object` | Parameters listing authorities or lines that preferably should not be used in trip patters. | *Optional* | | 2.2 |
|    [agencies](#rd_unpreferred_agencies) | `feed-scoped-id[]` | The ids of the agencies that incur an extra cost when being used. Format: `FeedId:AgencyId` | *Optional* | | 2.2 |
Expand Down Expand Up @@ -248,16 +248,16 @@ Penalty added for using every route that is not preferred if user set any route

We return number of seconds that we are willing to wait for preferred route.

<h3 id="rd_relaxTransitPriorityGroup">relaxTransitPriorityGroup</h3>
<h3 id="rd_relaxTransitGroupPriority">relaxTransitGroupPriority</h3>

**Since version:** `2.5`**Type:** `string`**Cardinality:** `Optional`**Default value:** `"0s + 1.00 t"`
**Path:** /routingDefaults

The relax function for transit-priority-groups
The relax function for transit-group-priority

A path is considered optimal if the generalized-cost is less than the
generalized-cost of another path. If this parameter is set, the comparison is relaxed
further if they belong to different transit-priority-groups.
A path is considered optimal if the generalized-cost is less than the generalized-cost of
another path. If this parameter is set, the comparison is relaxed further if they belong
to different transit groups.


<h3 id="rd_relaxTransitSearchGeneralizedCostAtDestination">relaxTransitSearchGeneralizedCostAtDestination</h3>
Expand Down Expand Up @@ -813,22 +813,20 @@ This enables the transfer wait time optimization.

If not enabled generalizedCost function is used to pick the optimal transfer point.

<h3 id="rd_transitPriorityGroups">transitPriorityGroups</h3>
<h3 id="rd_transitGroupPriority">transitGroupPriority</h3>

**Since version:** `2.5`**Type:** `object`**Cardinality:** `Optional`
**Path:** /routingDefaults

Transit priority groups configuration
Group transit patterns and give each group a mutual advantage in the Raptor search.

Use this to separate transit patterns into groups. Each group will be given a group-id. A
path (multiple legs) will then have a set of group-ids based on the group-id from each leg.
Hence, two paths with a different set of group-ids will BOTH be optimal unless the cost is
worse than the relaxation specified in the `relaxTransitPriorityGroup` parameter. This is
worse than the relaxation specified in the `relaxTransitGroupPriority` parameter. This is
only available in the TransmodelAPI for now.

Unmatched patterns are put in the BASE priority-group (group id: 0). This group is special.
If a path only have legs in the base group, then that path dominates other paths, but other
paths must be better to make it.
Unmatched patterns are put in the BASE priority-group.


**THIS IS STILL AN EXPERIMENTAL FEATURE - IT MAY CHANGE WITHOUT ANY NOTICE!**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static RaptorRequest<TripSchedule> enableHack(
return raptorRequest;
}

if (!request.preferences().transit().relaxTransitPriorityGroup().isNormal()) {
if (!request.preferences().transit().relaxTransitGroupPriority().isNormal()) {
return raptorRequest;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,8 @@ private BoardAndAlightSlack mapTransit() {
setIfNotNull(req.otherThanPreferredRoutesPenalty, tr::setOtherThanPreferredRoutesPenalty);
setIfNotNull(req.ignoreRealtimeUpdates, tr::setIgnoreRealtimeUpdates);

if (req.relaxTransitPriorityGroup != null) {
tr.withTransitGroupPriorityGeneralizedCostSlack(
CostLinearFunction.of(req.relaxTransitPriorityGroup)
);
if (req.relaxTransitGroupPriority != null) {
tr.withRelaxTransitGroupPriority(CostLinearFunction.of(req.relaxTransitGroupPriority));
} else {
setIfNotNull(
req.relaxTransitSearchGeneralizedCostAtDestination,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,8 @@ public abstract class RoutingResource {
@QueryParam("useVehicleParkingAvailabilityInformation")
protected Boolean useVehicleParkingAvailabilityInformation;

@QueryParam("relaxTransitPriorityGroup")
protected String relaxTransitPriorityGroup;
@QueryParam("relaxTransitGroupPriority")
protected String relaxTransitGroupPriority;

/**
* Whether non-optimal transit paths at the destination should be returned.
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 @@ -34,8 +37,11 @@ public static void mapTransitPreferences(
callWith.argument("includePlannedCancellations", transit::setIncludePlannedCancellations);
callWith.argument("includeRealtimeCancellations", transit::setIncludeRealtimeCancellations);
callWith.argument(
"relaxTransitPriorityGroup",
transit::withTransitGroupPriorityGeneralizedCostSlack
"relaxTransitGroupPriority",
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 @@ -193,6 +193,18 @@ public static GraphQLObjectType create(
.dataFetcher(env -> itinerary(env).getGeneralizedCost())
.build()
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
.name("generalizedCost2")
.description(
"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."
)
.type(Scalars.GraphQLInt)
.dataFetcher(env -> itinerary(env).getGeneralizedCost2().orElse(null))
.build()
)
.field(
GraphQLFieldDefinition
.newFieldDefinition()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,30 +277,30 @@ public static GraphQLFieldDefinition create(
.argument(
GraphQLArgument
.newArgument()
.name("relaxTransitPriorityGroup")
.name("relaxTransitGroupPriority")
.description(
"""
Relax generalized-cost when comparing trips with a different set of
transit-priority-groups. The groups are set server side for service-journey and
transit-group-priorities. The groups are set server side for service-journey and
can not be configured in the API. This mainly helps to return competition neutral
services. Long distance authorities are put in different transit-priority-groups.
services. Long distance authorities are put in different transit-groups.
This relaxes the comparison inside the routing engine for each stop-arrival. If two
paths have a different set of transit-priority-groups, then the generalized-cost
paths have a different set of transit-group-priorities, then the generalized-cost
comparison is relaxed. The final set of paths are filtered through the normal
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()
)
.type(RelaxCostType.INPUT_TYPE)
.defaultValueLiteral(
preferences.transit().relaxTransitPriorityGroup().isNormal()
preferences.transit().relaxTransitGroupPriority().isNormal()
? NullValue.of()
: RelaxCostType.valueOf(preferences.transit().relaxTransitPriorityGroup())
: RelaxCostType.valueOf(preferences.transit().relaxTransitGroupPriority())
)
.build()
)
Expand Down Expand Up @@ -523,7 +523,7 @@ public static GraphQLFieldDefinition create(
GraphQLArgument
.newArgument()
.name("relaxTransitSearchGeneralizedCostAtDestination")
.deprecate("This is replaced by 'relaxTransitPriorityGroup'.")
.deprecate("This is replaced by 'relaxTransitGroupPriority'.")
.description(
"""
Whether non-optimal transit paths at the destination should be returned. Let c be the
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);
}
};
}
}
Loading

0 comments on commit fc6d1cd

Please sign in to comment.