Skip to content
Open
Show file tree
Hide file tree
Changes from 28 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 @@ -32,6 +32,7 @@
import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.AccessEgressMapper;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.DirectTransitRequestMapper;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.RaptorRequestMapper;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.DefaultTransitDataProviderFilter;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData;
Expand Down Expand Up @@ -136,7 +137,8 @@ private TransitRouterResult route() {
);

// Prepare transit search
var raptorRequest = RaptorRequestMapper.<TripSchedule>mapRequest(

var mapper = RaptorRequestMapper.<TripSchedule>of(
request,
transitSearchTimeZero,
serverContext.raptorConfig().isMultiThreaded(),
Expand All @@ -147,8 +149,9 @@ private TransitRouterResult route() {
this::listStopIndexes,
linkingContext
);
var raptorRequest = mapper.mapRaptorRequest();

// Route transit
// Transit routing using Raptor
var raptorService = new RaptorService<>(
serverContext.raptorConfig(),
createExtraMcRouterSearch(accessEgresses, raptorTransitData)
Expand All @@ -157,9 +160,26 @@ private TransitRouterResult route() {

checkIfTransitConnectionExists(transitResponse);

Collection<RaptorPath<TripSchedule>> paths = transitResponse.paths();

debugTimingAggregator.finishedRaptorSearch();

Collection<RaptorPath<TripSchedule>> paths = transitResponse.paths();
// Route Direct transit
var directRequest = DirectTransitRequestMapper.map(
request,
transitResponse.requestUsed().searchParams()
);
if (directRequest.isPresent()) {
debugTimingAggregator.startedDirectTransitSearch();
var directPaths = raptorService.findAllDirectTransit(
directRequest.get(),
requestTransitDataProvider
);
paths = new ArrayList<>(paths);
paths.addAll(directPaths);
debugTimingAggregator.finishedDirectTransitSearch();
}
debugTimingAggregator.startedItineraryCreation();

// TODO VIA - Temporarily turn OptimizeTransfers OFF for VIA search until the service support via
// Remove '&& !request.isViaSearch()'
Expand All @@ -179,7 +199,7 @@ private TransitRouterResult route() {
request.preferences().transfer().optimization(),
raptorRequest.searchParams().viaLocations()
);
paths = service.optimize(transitResponse.paths());
paths = service.optimize(paths);
}

// Create itineraries
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.opentripplanner.routing.algorithm.raptoradapter.transit;

import org.opentripplanner.raptor.api.model.AbstractAccessEgressDecorator;
import org.opentripplanner.raptor.api.model.RaptorAccessEgress;

/**
* This decorator will add an extra cost factor to the c1 of the access/egress.
*/
public class AccessEgressWithExtraCost extends AbstractAccessEgressDecorator {

private final double costFactor;

public AccessEgressWithExtraCost(RaptorAccessEgress delegate, double costFactor) {
super(delegate);
this.costFactor = costFactor;
}

@Override
public int c1() {
return (int) (delegate().c1() * costFactor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers;

import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.opentripplanner.raptor.api.model.RaptorAccessEgress;
import org.opentripplanner.raptor.api.request.SearchParams;
import org.opentripplanner.raptor.direct.api.RaptorDirectTransitRequest;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.AccessEgressWithExtraCost;
import org.opentripplanner.routing.api.request.RouteRequest;

public class DirectTransitRequestMapper {

/// Map the request into a request object for the direct transit search. Will return empty if
/// the direct transit search shouldn't be run.
public static Optional<RaptorDirectTransitRequest> map(
RouteRequest request,
SearchParams searchParamsUsed
) {
var directTransitRequestOpt = request.preferences().transit().directTransit();
if (directTransitRequestOpt.isEmpty()) {
return Optional.empty();
}
var rel = directTransitRequestOpt.orElseThrow();
Collection<? extends RaptorAccessEgress> access = searchParamsUsed.accessPaths();
Collection<? extends RaptorAccessEgress> egress = searchParamsUsed.egressPaths();

access = filterAccessEgressNoOpeningHours(access);
egress = filterAccessEgressNoOpeningHours(egress);

if (rel.maxAccessEgressDuration().isPresent()) {
var maxDuration = rel.maxAccessEgressDuration().get();
access = filterAccessEgressByDuration(access, maxDuration);
egress = filterAccessEgressByDuration(egress, maxDuration);
}
if (rel.isExtraReluctanceAddedToAccessAndEgress()) {
double f = rel.extraAccessEgressReluctance();
access = decorateAccessEgressWithExtraCost(access, f);
egress = decorateAccessEgressWithExtraCost(egress, f);
}
if (access.isEmpty() || egress.isEmpty()) {
return Optional.empty();
}
var directRequest = RaptorDirectTransitRequest.of()
.addAccessPaths(access)
.addEgressPaths(egress)
.searchWindowInSeconds(searchParamsUsed.searchWindowInSeconds())
.earliestDepartureTime(searchParamsUsed.earliestDepartureTime())
.withRelaxC1(RaptorRequestMapper.mapRelaxCost(rel.costRelaxFunction()))
.build();
return Optional.of(directRequest);
}

private static List<? extends RaptorAccessEgress> filterAccessEgressByDuration(
Collection<? extends RaptorAccessEgress> list,
Duration maxDuration
) {
return list
.stream()
.filter(ae -> ae.durationInSeconds() <= maxDuration.toSeconds())
.toList();
}

private static List<? extends RaptorAccessEgress> filterAccessEgressNoOpeningHours(
Collection<? extends RaptorAccessEgress> list
) {
return list
.stream()
.filter(it -> !it.hasOpeningHours())
.toList();
}

private static List<? extends RaptorAccessEgress> decorateAccessEgressWithExtraCost(
Collection<? extends RaptorAccessEgress> list,
double costFactor
) {
return list
.stream()
.map(it -> new AccessEgressWithExtraCost(it, costFactor))
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private RaptorRequestMapper(
this.linkingContext = Objects.requireNonNull(linkingContext);
}

public static <T extends RaptorTripSchedule> RaptorRequest<T> mapRequest(
public static <T extends RaptorTripSchedule> RaptorRequestMapper<T> of(
RouteRequest request,
ZonedDateTime transitSearchTimeZero,
boolean isMultiThreaded,
Expand All @@ -97,10 +97,10 @@ public static <T extends RaptorTripSchedule> RaptorRequest<T> mapRequest(
viaTransferResolver,
lookUpStopIndex,
linkingContext
).doMap();
);
}

private RaptorRequest<T> doMap() {
public RaptorRequest<T> mapRaptorRequest() {
var builder = new RaptorRequestBuilder<T>();
var searchParams = builder.searchParams();
var preferences = request.preferences();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package org.opentripplanner.routing.api.request.preference;

import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import org.opentripplanner.framework.model.Cost;
import org.opentripplanner.routing.api.request.framework.CostLinearFunction;
import org.opentripplanner.utils.tostring.ToStringBuilder;

/// All preferences related to the Direct Transit Search
public class DirectTransitPreferences {

// The next constants are package-local to be readable in the unit-test.
static final double DEFAULT_RELUCTANCE = 1.0;
static final CostLinearFunction DEFAULT_COST_RELAX_FUNCTION = CostLinearFunction.of(
Cost.costOfMinutes(15),
1.5
);

public static final DirectTransitPreferences DEFAULT = new DirectTransitPreferences(
false,
DEFAULT_COST_RELAX_FUNCTION,
DEFAULT_RELUCTANCE,
null
);

private final boolean enabled;
private final CostLinearFunction costRelaxFunction;
private final double extraAccessEgressReluctance;

@Nullable
private final Duration maxAccessEgressDuration;

private DirectTransitPreferences(
boolean enabled,
CostLinearFunction costRelaxFunction,
double extraAccessEgressReluctance,
Duration maxAccessEgressDuration
) {
this.enabled = enabled;
this.costRelaxFunction = Objects.requireNonNull(costRelaxFunction);
this.extraAccessEgressReluctance = extraAccessEgressReluctance;
this.maxAccessEgressDuration = Objects.requireNonNull(maxAccessEgressDuration);
}

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

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

/// Whether to enable direct transit 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 extraAccessEgressReluctance() {
return extraAccessEgressReluctance;
}

/// Whether there is any extra access/egress reluctance
public boolean isExtraReluctanceAddedToAccessAndEgress() {
return extraAccessEgressReluctance != DEFAULT_RELUCTANCE;
}

/// A limit on the duration for access/egress for this search. Setting this to 0 will only include
/// results that require no access or egress. I.e. a stop-to-stop search.
public Optional<Duration> maxAccessEgressDuration() {
return Optional.ofNullable(maxAccessEgressDuration);
}

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

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

@Override
public String toString() {
return ToStringBuilder.of(DirectTransitPreferences.class)
.addBool("enabled", enabled)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we only log this when it doesn't match the default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful to log it even when it is default (false) since this will cause the direct transit search to be disabled.

.addObj("costRelaxFunction", costRelaxFunction, DEFAULT.costRelaxFunction)
.addNum(
"extraAccessEgressReluctance",
extraAccessEgressReluctance,
DEFAULT.extraAccessEgressReluctance
)
.addDuration(
"maxAccessEgressDuration",
maxAccessEgressDuration,
DEFAULT.maxAccessEgressDuration
)
.toString();
}

public static class Builder {

private boolean enabled;
private CostLinearFunction costRelaxFunction;
private double extraAccessEgressReluctance;
private Duration maxAccessEgressDuration;
public DirectTransitPreferences original;

public Builder(DirectTransitPreferences original) {
this.original = original;
this.enabled = original.enabled;
this.costRelaxFunction = original.costRelaxFunction;
this.extraAccessEgressReluctance = original.extraAccessEgressReluctance;
this.maxAccessEgressDuration = original.maxAccessEgressDuration;
}

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

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

public Builder withExtraAccessEgressReluctance(double extraAccessEgressReluctance) {
this.extraAccessEgressReluctance = extraAccessEgressReluctance;
return this;
}

public Builder withMaxAccessEgressDuration(Duration maxAccessEgressDuration) {
this.maxAccessEgressDuration = maxAccessEgressDuration;
return this;
}

public DirectTransitPreferences build() {
var value = new DirectTransitPreferences(
enabled,
costRelaxFunction,
extraAccessEgressReluctance,
maxAccessEgressDuration
);
return original.equals(value) ? original : value;
}
}
}
Loading
Loading