Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bfe7155
feat: Implement RelaxedLimitedTransferSearch
habrahamsson-skanetrafiken Dec 18, 2025
2207936
feat: Add configuration for RelaxedLimitedTransferSearch
habrahamsson-skanetrafiken Dec 18, 2025
4588ff4
test: Tests for RelaxedLimitedTransferSearch
habrahamsson-skanetrafiken Dec 18, 2025
6081246
feat: Implement direct transit search and refactor related components
t2gran Jan 12, 2026
86a578e
feat: Add debug timers
habrahamsson-skanetrafiken Jan 12, 2026
b6a90f3
feat: Remove opening hour access/egress from direct transit search
habrahamsson-skanetrafiken Jan 12, 2026
bad1eed
Fix build
habrahamsson-skanetrafiken Jan 12, 2026
1bd2aaa
Fix test
habrahamsson-skanetrafiken Jan 13, 2026
14c38fd
Regen doc
habrahamsson-skanetrafiken Jan 13, 2026
56f75f0
restore the enabled configuration flag
habrahamsson-skanetrafiken Jan 14, 2026
9552764
fix: Fix for direct transit results not showing up
habrahamsson-skanetrafiken Jan 14, 2026
8f2ecc2
change disableAcessEgress to maxAccessEgressDuration
habrahamsson-skanetrafiken Jan 15, 2026
5bcfb71
Remove unused code
habrahamsson-skanetrafiken Jan 15, 2026
57f1220
Add some documentation
habrahamsson-skanetrafiken Jan 15, 2026
02ad356
Minor changes to RaptorDirectTransitRequest
habrahamsson-skanetrafiken Jan 20, 2026
bdc915c
Dont run search if access/egress is empty
habrahamsson-skanetrafiken Jan 20, 2026
3e525f4
refactor: Remove unused RelaxedLimitedTransferRequest and rename test.
t2gran Jan 22, 2026
4f21b2e
Address review comments
habrahamsson-skanetrafiken Jan 23, 2026
d500b70
Merge branch 'dev-2.x' into fix-direct
habrahamsson-skanetrafiken Jan 23, 2026
1abe08a
Small fixes after merge
habrahamsson-skanetrafiken Jan 23, 2026
30a9e4c
refactor: Skip trips in trip-search for direct transit search
t2gran Jan 23, 2026
9527457
refactor: Separate mapping logic
habrahamsson-skanetrafiken Jan 27, 2026
59f8755
Fix documentation
habrahamsson-skanetrafiken Jan 27, 2026
8221011
refactor: Rename CostFactor to Reluctance
habrahamsson-skanetrafiken Jan 27, 2026
75be5cf
Update preferences object
habrahamsson-skanetrafiken Jan 27, 2026
2fef52c
Adress review comments
habrahamsson-skanetrafiken Jan 27, 2026
4cb1c54
Update application/src/main/java/org/opentripplanner/routing/algorith…
habrahamsson-skanetrafiken Jan 29, 2026
bf33980
Add constructor non-null validation
habrahamsson-skanetrafiken Jan 29, 2026
4c2df61
Fix nullable parameter
habrahamsson-skanetrafiken Jan 29, 2026
46fe8ee
Don't log the enabled flag when default
habrahamsson-skanetrafiken Jan 30, 2026
4fc568a
refactor: Only log DirectTransitPreferences when not default
t2gran Jan 30, 2026
64cf3b4
Fix formatting
habrahamsson-skanetrafiken Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@ private RaptorRequest<T> doMap() {
if (pt.isRelaxTransitGroupPrioritySet() && !hasPassThroughOnly()) {
mapRelaxTransitGroupPriority(mcBuilder, pt);
}

var rel = pt.relaxedLimitedTransferSearch();
if (rel.enabled()) {
mcBuilder.withRelaxedLimitedTransferRequest(relaxedSearch ->
relaxedSearch
.withEnabled(true)
.withCostRelaxFunction(mapRelaxCost(rel.costRelaxFunction()))
.withDisableAccessEgress(rel.disableAccessEgress())
.withExtraAccessEgressCostFactor(rel.extraAccessEgressCostFactor())
);
}
});

for (Optimization optimization : preferences.transit().raptor().optimizations()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.opentripplanner.routing.api.request.preference;

import java.util.Objects;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;

public class RelaxedLimitedTransferPreferences {

public static final RelaxedLimitedTransferPreferences DEFAULT =
new RelaxedLimitedTransferPreferences();

private final boolean enabled;
private final CostLinearFunction costRelaxFunction;
private final double extraAccessEgressCostFactor;
private final boolean disableAccessEgress;

private RelaxedLimitedTransferPreferences() {
this.enabled = false;
this.costRelaxFunction = CostLinearFunction.of(Cost.costOfMinutes(15), 1.5);
this.extraAccessEgressCostFactor = 1.0;
this.disableAccessEgress = false;
}

RelaxedLimitedTransferPreferences(Builder builder) {
this.enabled = builder.enabled;
this.costRelaxFunction = builder.costRelaxFunction;
this.extraAccessEgressCostFactor = builder.extraAccessEgressCostFactor;
this.disableAccessEgress = builder.disableAccessEgress;
}

public static Builder of() {
return new Builder(new RelaxedLimitedTransferPreferences());
}

/// Whether to enable relaxed limited transfer search
public boolean enabled() {
return enabled;
}

/// This is used to limit the results from the search. Paths are compared with the cheapest path
/// in the search window and are included in the result if they fall within the limit given by the
/// costRelaxFunction.
public CostLinearFunction costRelaxFunction() {
return costRelaxFunction;
}

/// An extra cost that is used to increase the cost of the access/egress legs for this search.
public double extraAccessEgressCostFactor() {
return extraAccessEgressCostFactor;
}

/// If access egress is disabled the search will only include results that require no access or
/// egress. I.e. a stop-to-stop search.
public boolean disableAccessEgress() {
return disableAccessEgress;
}

public Builder copyOf() {
return new Builder(this);
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
RelaxedLimitedTransferPreferences that = (RelaxedLimitedTransferPreferences) o;
return (
enabled == that.enabled &&
Double.compare(extraAccessEgressCostFactor, that.extraAccessEgressCostFactor) == 0 &&
disableAccessEgress == that.disableAccessEgress &&
Objects.equals(costRelaxFunction, that.costRelaxFunction)
);
}

@Override
public int hashCode() {
return Objects.hash(
enabled,
costRelaxFunction,
extraAccessEgressCostFactor,
disableAccessEgress
);
}

public static class Builder {

private boolean enabled;
private CostLinearFunction costRelaxFunction;
private double extraAccessEgressCostFactor;
private boolean disableAccessEgress;

public Builder(RelaxedLimitedTransferPreferences original) {
this.enabled = original.enabled;
this.costRelaxFunction = original.costRelaxFunction;
this.extraAccessEgressCostFactor = original.extraAccessEgressCostFactor;
this.disableAccessEgress = original.disableAccessEgress;
}

public Builder withEnabled(boolean enabled) {
this.enabled = enabled;
return this;
}

public Builder withCostRelaxFunction(CostLinearFunction costRelaxFunction) {
this.costRelaxFunction = costRelaxFunction;
return this;
}

public Builder withExtraAccessEgressCostFactor(double extraAccessEgressCostFactor) {
this.extraAccessEgressCostFactor = extraAccessEgressCostFactor;
return this;
}

public Builder withDisableAccessEgress(boolean disableAccessEgress) {
this.disableAccessEgress = disableAccessEgress;
return this;
}

public RelaxedLimitedTransferPreferences build() {
return new RelaxedLimitedTransferPreferences(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public final class TransitPreferences implements Serializable {
private final boolean includePlannedCancellations;
private final boolean includeRealtimeCancellations;
private final RaptorPreferences raptor;
private final RelaxedLimitedTransferPreferences relaxedLimitedTransferSearch;

private TransitPreferences() {
this.boardSlack = this.alightSlack = DurationForEnum.of(TransitMode.class).build();
Expand All @@ -42,6 +43,7 @@ private TransitPreferences() {
this.includePlannedCancellations = false;
this.includeRealtimeCancellations = false;
this.raptor = RaptorPreferences.DEFAULT;
this.relaxedLimitedTransferSearch = RelaxedLimitedTransferPreferences.DEFAULT;
}

private TransitPreferences(Builder builder) {
Expand All @@ -55,6 +57,7 @@ private TransitPreferences(Builder builder) {
this.includePlannedCancellations = builder.includePlannedCancellations;
this.includeRealtimeCancellations = builder.includeRealtimeCancellations;
this.raptor = requireNonNull(builder.raptor);
this.relaxedLimitedTransferSearch = requireNonNull(builder.relaxedLimitedTransferSearch);
}

public static Builder of() {
Expand Down Expand Up @@ -169,6 +172,10 @@ public RaptorPreferences raptor() {
return raptor;
}

public RelaxedLimitedTransferPreferences relaxedLimitedTransferSearch() {
return relaxedLimitedTransferSearch;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -188,7 +195,8 @@ public boolean equals(Object o) {
ignoreRealtimeUpdates == that.ignoreRealtimeUpdates &&
includePlannedCancellations == that.includePlannedCancellations &&
includeRealtimeCancellations == that.includeRealtimeCancellations &&
raptor.equals(that.raptor)
raptor.equals(that.raptor) &&
relaxedLimitedTransferSearch.equals(that.relaxedLimitedTransferSearch)
);
}

Expand All @@ -204,7 +212,8 @@ public int hashCode() {
ignoreRealtimeUpdates,
includePlannedCancellations,
includeRealtimeCancellations,
raptor
raptor,
relaxedLimitedTransferSearch
);
}

Expand Down Expand Up @@ -252,6 +261,7 @@ public static class Builder {
private boolean includePlannedCancellations;
private boolean includeRealtimeCancellations;
private RaptorPreferences raptor;
private RelaxedLimitedTransferPreferences relaxedLimitedTransferSearch;

public Builder(TransitPreferences original) {
this.original = original;
Expand All @@ -265,6 +275,7 @@ public Builder(TransitPreferences original) {
this.includePlannedCancellations = original.includePlannedCancellations;
this.includeRealtimeCancellations = original.includeRealtimeCancellations;
this.raptor = original.raptor;
this.relaxedLimitedTransferSearch = original.relaxedLimitedTransferSearch;
}

public TransitPreferences original() {
Expand Down Expand Up @@ -334,6 +345,15 @@ public Builder withRaptor(Consumer<RaptorPreferences.Builder> body) {
return this;
}

public Builder withRelaxedLimitedTransferSearch(
Consumer<RelaxedLimitedTransferPreferences.Builder> body
) {
var builder = relaxedLimitedTransferSearch.copyOf();
body.accept(builder);
this.relaxedLimitedTransferSearch = builder.build();
return this;
}

public Builder apply(Consumer<Builder> body) {
body.accept(this);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_4;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_7;
import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_9;
import static org.opentripplanner.standalone.config.routerequest.ItineraryFiltersConfig.mapItineraryFilterParams;
import static org.opentripplanner.standalone.config.routerequest.TransferConfig.mapTransferPreferences;
import static org.opentripplanner.standalone.config.routerequest.TriangleOptimizationConfig.mapOptimizationTriangle;
Expand All @@ -26,6 +27,7 @@
import org.opentripplanner.routing.api.request.preference.BikePreferences;
import org.opentripplanner.routing.api.request.preference.CarPreferences;
import org.opentripplanner.routing.api.request.preference.EscalatorPreferences;
import org.opentripplanner.routing.api.request.preference.RelaxedLimitedTransferPreferences;
import org.opentripplanner.routing.api.request.preference.RoutingPreferencesBuilder;
import org.opentripplanner.routing.api.request.preference.ScooterPreferences;
import org.opentripplanner.routing.api.request.preference.StreetPreferences;
Expand Down Expand Up @@ -339,6 +341,82 @@ A related parameter (transferSlack) also helps avoid missed connections when the
if (relaxTransitGroupPriorityValue != null) {
builder.withRelaxTransitGroupPriority(CostLinearFunction.of(relaxTransitGroupPriorityValue));
}

builder.withRelaxedLimitedTransferSearch(it ->
mapRelaxedLimitedTransferSearchPreferences(c, it)
);
}

private static void mapRelaxedLimitedTransferSearchPreferences(
NodeAdapter root,
RelaxedLimitedTransferPreferences.Builder builder
) {
NodeAdapter c = root
.of("relaxedLimitedTransferSearch")
.since(V2_9)
.summary("Extend the search result with extra paths using a limited number of transit legs")
.description(
"""
The relaxed limited transfer search finds paths using a single transit leg, limited to a
specified cost window. It will find paths even if they are not optimal in regard to the
criteria in the main raptor search.
"""
)
.asObject();

if (c.isEmpty()) {
return;
}
var dft = RelaxedLimitedTransferPreferences.DEFAULT;

builder
.withEnabled(
c
.of("enabled")
.since(V2_9)
.summary("Enable the relaxed limited transfer search")
.asBoolean(dft.enabled())
)
.withCostRelaxFunction(
c
.of("costRelaxFunction")
.since(V2_9)
.summary("The cost window for which paths to include.")
.description(
"""
A cost relax function of `10m + 2x` will include paths that have a cost up to 2 times plus
10 minutes compared to the cheapest path. I.e. if the cheapest path has a cost of 100m
the results will include paths with a cost 210m.
"""
)
.asCostLinearFunction(dft.costRelaxFunction())
)
.withExtraAccessEgressCostFactor(
c
.of("extraAccessEgressCostFactor")
.since(V2_9)
.summary("Add an extra cost to access/egress legs for these results")
.description(
"""
The cost for access/egress will be multiplied by this factor. This can be used to limit
the amount of walking in the paths.
"""
)
.asDouble(dft.extraAccessEgressCostFactor())
)
.withDisableAccessEgress(
c
.of("disableAccessEgress")
.since(V2_9)
.summary("Only add paths for stop to stop searches")
.description(
"""
Don't include paths where access or egress is necessary. In this case the search will
only be used when searching to and from a stop or station.
"""
)
.asBoolean(dft.disableAccessEgress())
);
}

private static void mapBikePreferences(NodeAdapter root, BikePreferences.Builder builder) {
Expand Down
13 changes: 13 additions & 0 deletions doc/user/RouteRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ and in the [transferRequests in build-config.json](BuildConfiguration.md#transfe
|       [costLimitFunction](#rd_if_transitGeneralizedCostLimit_costLimitFunction) | `cost-linear-function` | The base function used by the filter. | *Optional* | `"15m + 1.50 t"` | 2.2 |
|       [intervalRelaxFactor](#rd_if_transitGeneralizedCostLimit_intervalRelaxFactor) | `double` | How much the filter should be relaxed for itineraries that do not overlap in time. | *Optional* | `0.4` | 2.2 |
| [maxDirectStreetDurationForMode](#rd_maxDirectStreetDurationForMode) | `enum map of duration` | Limit direct route duration per street mode. | *Optional* | | 2.2 |
| [relaxedLimitedTransferSearch](#rd_relaxedLimitedTransferSearch) | `object` | Extend the search result with extra paths using a limited number of transit legs | *Optional* | | 2.9 |
| scooter | `object` | Scooter preferences. | *Optional* | | 2.5 |
|    [optimization](#rd_scooter_optimization) | `enum` | The set of characteristics that the user wants to optimize for. | *Optional* | `"safe-streets"` | 2.0 |
|    reluctance | `double` | A multiplier for how bad scooter travel is, compared to being in transit for equal lengths of time. | *Optional* | `2.0` | 2.0 |
Expand Down Expand Up @@ -923,6 +924,18 @@ Override the settings in `maxDirectStreetDuration` for specific street modes. Th
done because some street modes searches are much more resource intensive than others.


<h3 id="rd_relaxedLimitedTransferSearch">relaxedLimitedTransferSearch</h3>

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

Extend the search result with extra paths using a limited number of transit legs

The relaxed limited transfer search finds paths using a single transit leg, limited to a
specified cost window. It will find paths even if they are not optimal in regard to the
criteria in the main raptor search.


<h3 id="rd_scooter_optimization">optimization</h3>

**Since version:** `2.0` ∙ **Type:** `enum` ∙ **Cardinality:** `Optional` ∙ **Default value:** `"safe-streets"`
Expand Down
Loading
Loading