Skip to content

feat: implement multi transport types for the same dataset - #2579

Open
diogodanielsoaresferreira wants to merge 13 commits into
mainfrom
draft/transport-type-aware-location
Open

diogodanielsoaresferreira wants to merge 13 commits into
mainfrom
draft/transport-type-aware-location

Conversation

@diogodanielsoaresferreira

@diogodanielsoaresferreira diogodanielsoaresferreira commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Allow to solve routing problems with more than one transportType.

Created additional properties in Location so that we keep the hot path without the Map access.

Related with https://github.com/TimefoldAI/timefold-platform/issues/2690

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
31.1% Coverage on New Code (required ≥ 70%)

See analysis details on SonarQube Cloud

Add a TransportType value type and per-mode travel-time/distance matrices
on Location, so the same routing problem can use more than one transport
profile (e.g. car + bike). The default CAR mode keeps the existing scalar/
timeframe fields and index-cache fast path; extra modes are stored in
opt-in maps that stay null for single-mode problems.

The TravelTimeMatrixEnricher fetches one matrix set per transport type
(each resolving to its own OSRM instance), and map-service.transport-type
now accepts a comma-separated list of modes (first entry is primary).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@diogodanielsoaresferreira
diogodanielsoaresferreira marked this pull request as ready for review September 24, 2026 09:13
@diogodanielsoaresferreira
diogodanielsoaresferreira requested a review from a team September 24, 2026 09:13
@diogodanielsoaresferreira diogodanielsoaresferreira changed the title Draft/transport type aware location feat: implement multi transport types for the same dataset Sep 24, 2026
@timefold-automations

This comment has been minimized.

@timefold-automations

This comment has been minimized.

@timefold-automations

This comment has been minimized.

@timefold-automations

Copy link
Copy Markdown

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

gh-aw-workflow-id: solver-docs-drift-review

The documentation drift reported earlier is gone; this pull request now covers the transport-type API and multi-transport enrichment behavior.

Generated by Solver Docs Drift Review · copilot · gpt56 · 8.94 AIC · ⊞ 25.2K · ◷

@Schema(description = "The type of transport used (car, bicycle, ... ).")
public enum TransportType {

CAR("car"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need the lower-case value? If we drop it, we don't need to override the enum methods nor creating the constructor.


@JsonCreator
public static TransportType of(String value) {
Objects.requireNonNull(value, "TransportType value must not be null.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Compared to the constructor, we don't check if the value is empty.

But see my proposal about simplifying the enum, which would leave only this method and eliminate the constructor.

String maxDistanceFromRoadOption = maxDistanceFromRoad.map(MapServiceOptions::getMaxDistanceFromRoadOption).orElse("");
String transportTypeOption = transportType.map(MapServiceOptions::getTransportTypeOption).orElse("");
String transportTypeOption = transportType == null || transportType.isAutoSelect()
? ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, for the map service, the default is auto-select?


public String getOptions(TransportType transportType) {
return getOptions((String) null, transportType);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of adding overloaded methods and passing nulls to override what's coming from the configuration parameters, we may decouple the config from these getOptions() parameters by introducing a builder that registers the overrides and only then builds the options String.

List<Location> getLocations();

default List<TransportType> getTransportTypes() {
return List.of();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If this leads to auto-select, let's return it here already; it will become more obvious.

Comment on lines +176 to +178
A deployment may additionally be restricted to a subset of transport types with
`timefold.platform.map-service.allowed-transport-types`, a comma-separated list; when it is not set, every transport
type is allowed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Who restricts that?

I assume this comes as a platform config, in which case the model developer cannot much influence that if they don't run the model locally.

Comment on lines +201 to +206
If your model stores matrices itself rather than relying on the enricher, the setters take the transport type too:

[source,java,options="nowrap"]
----
location.setTravelTimeMatrix(TransportType.BICYCLE, travelTimeMatrix);
location.setDistanceMatrix(TransportType.BICYCLE, distanceMatrix);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's not expose this as an API. We should take care of the matrices, not the user.

Comment on lines +227 to +238
Each of those transport types is validated against the allowed transport types and then fetched in its own map service
request, so a dataset using two transport types results in two requests and two sets of matrices per `Location`.
A dataset that uses a transport type the deployment is not allowed to route with is rejected before any request is made.
When the solution returns an empty list, the default transport type is used.

One of the transport types is the *primary* one: the configured one when it is fixed, otherwise `car` if the dataset
uses it, and the first one in declaration order otherwise. The primary transport type is special in two ways:

* The model-level map metadata — the locations that are not in the map, and the resolved map region — comes from its
request only.
* Its matrices also back the lookups that do not name a transport type, so those keep answering on a deployment
configured for a single non-default transport type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why should the user care? This text explains how we implement the transport types, but that's the complexity we are trying to remove from the user.

Comment on lines +423 to +458
private DistanceMatrix travelTimeMatrixForMode(TransportType transportType, OffsetDateTime departureTime) {
if (isDefaultMode(transportType)) {
return hasTimeframeMatrices(travelTimesByTimeframe)
? resolveTimeframeMatrix(travelTimesByTimeframe, departureTime, "travel time")
: travelTimeMatrix;
}
DistanceMatrix[] byTimeframe = travelTimesByTimeframeByMode == null
? null
: travelTimesByTimeframeByMode[transportType.ordinal()];
if (byTimeframe != null && timeframeIndexResolver != null) {
return resolveTimeframeMatrix(byTimeframe, departureTime, "travel time");
}
return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode[transportType.ordinal()];
}

private DistanceMatrix distanceMatrixForMode(TransportType transportType) {
if (isDefaultMode(transportType)) {
return distanceMatrix;
}
return distanceMatrixByMode == null ? null : distanceMatrixByMode[transportType.ordinal()];
}

private DistanceMatrix distanceMatrixForMode(TransportType transportType, OffsetDateTime departureTime) {
if (isDefaultMode(transportType)) {
return hasTimeframeMatrices(distancesByTimeframe)
? resolveTimeframeMatrix(distancesByTimeframe, departureTime, "distance")
: distanceMatrix;
}
DistanceMatrix[] byTimeframe = distancesByTimeframeByMode == null
? null
: distancesByTimeframeByMode[transportType.ordinal()];
if (byTimeframe != null && timeframeIndexResolver != null) {
return resolveTimeframeMatrix(byTimeframe, departureTime, "distance");
}
return distanceMatrixByMode == null ? null : distanceMatrixByMode[transportType.ordinal()];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

According to SonarCloud, these methods that resolve the distance matrix are not covered by tests. We likely miss more tests that would indirectly trigger them.

return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode[transportType.ordinal()];
}

private DistanceMatrix travelTimeMatrixForMode(TransportType transportType, OffsetDateTime departureTime) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we are adding more logic to resolve the matrix on the hot path, do we have any benchmarks showing the impact on move evaluation speed for multiple vs. a single transport type?

This branch was successfully deployed

2 active deployments
documentation (preview) — b2a8e835 Deployed Oct 1, 2026 by diogodanielsoaresferreira via Build Documentation #735
internal — b2a8e835 Deployed Oct 1, 2026 by diogodanielsoaresferreira via approval_required #735
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants