Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,6 +1,8 @@
package ai.timefold.solver.service.maps.api.model;

import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.function.ToIntFunction;

import jakarta.validation.constraints.Max;
Expand Down Expand Up @@ -50,6 +52,20 @@
@JsonIgnore
private ToIntFunction<OffsetDateTime> timeframeIndexResolver;

// Non-default transport types only. The default mode ({@link TransportType#CAR}) continues to use the scalar/
// timeframe fields above so its lookups keep the IndexableDistanceMatrix index-cache fast path.
@JsonIgnore
private Map<TransportType, DistanceMatrix> travelTimeMatrixByMode;

@JsonIgnore
private Map<TransportType, DistanceMatrix> distanceMatrixByMode;

@JsonIgnore
private Map<TransportType, DistanceMatrix[]> travelTimesByTimeframeByMode;

@JsonIgnore
private Map<TransportType, DistanceMatrix[]> distancesByTimeframeByMode;

public Location() {
}

Expand Down Expand Up @@ -120,6 +136,54 @@
}
}

public void setTravelTimeMatrix(TransportType transportType, DistanceMatrix travelTimeMatrix) {
if (isDefaultMode(transportType)) {
setTravelTimeMatrix(travelTimeMatrix);
return;
}
if (travelTimeMatrixByMode == null) {
travelTimeMatrixByMode = new HashMap<>();

Check warning on line 145 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Convert this Map to an EnumMap.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBr1hBIp8EFLDUUZgJY&open=AaBr1hBIp8EFLDUUZgJY&pullRequest=2579
}
travelTimeMatrixByMode.put(transportType, travelTimeMatrix);
}

public void setDistanceMatrix(TransportType transportType, DistanceMatrix distanceMatrix) {
if (isDefaultMode(transportType)) {
setDistanceMatrix(distanceMatrix);
return;
}
if (distanceMatrixByMode == null) {
distanceMatrixByMode = new HashMap<>();

Check warning on line 156 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Convert this Map to an EnumMap.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBr1hBIp8EFLDUUZgJZ&open=AaBr1hBIp8EFLDUUZgJZ&pullRequest=2579
}
distanceMatrixByMode.put(transportType, distanceMatrix);
}

public void setTravelTimeMatrices(TransportType transportType, DistanceMatrix[] travelTimesByTimeframe,
ToIntFunction<OffsetDateTime> indexResolver) {
if (isDefaultMode(transportType)) {
setTravelTimeMatrices(travelTimesByTimeframe, indexResolver);
return;
}
if (travelTimesByTimeframeByMode == null) {
travelTimesByTimeframeByMode = new HashMap<>();

Check warning on line 168 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Convert this Map to an EnumMap.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBr1hBIp8EFLDUUZgJa&open=AaBr1hBIp8EFLDUUZgJa&pullRequest=2579
}
travelTimesByTimeframeByMode.put(transportType, travelTimesByTimeframe);
this.timeframeIndexResolver = indexResolver;
}

public void setDistanceMatrices(TransportType transportType, DistanceMatrix[] distancesByTimeframe,
ToIntFunction<OffsetDateTime> indexResolver) {
if (isDefaultMode(transportType)) {
setDistanceMatrices(distancesByTimeframe, indexResolver);
return;
}
if (distancesByTimeframeByMode == null) {
distancesByTimeframeByMode = new HashMap<>();

Check warning on line 181 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Convert this Map to an EnumMap.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBr1hBIp8EFLDUUZgJb&open=AaBr1hBIp8EFLDUUZgJb&pullRequest=2579
}
distancesByTimeframeByMode.put(transportType, distancesByTimeframe);
this.timeframeIndexResolver = indexResolver;
}

/**
* Returns the travel time for a route between this location and the given location.
*
Expand Down Expand Up @@ -226,6 +290,69 @@
return TravelDistance.of(distance);
}

/**
* Returns the travel time for a route between this location and the given location using the given transport type.
*
* @param location the location representing the route destination
* @param transportType the routing profile to use; {@code null} is treated as {@link TransportType#CAR}
* @return {@link TravelTime} instance representing the travel time in seconds.
* @throws IllegalArgumentException When the resolved matrix does not include both locations.
* @throws IllegalStateException When no travel time matrix is configured for the given transport type.
*/
public TravelTime getTravelTimeTo(Location location, TransportType transportType) {
DistanceMatrix matrix = travelTimeMatrixForMode(transportType);
return TravelTime.of(lookup(matrix, location, transportType, "travel time", null));
}

/**
* Returns the travel time for a route between this location and the given location at the given departure time,
* using the given transport type.
*
* @param location the location representing the route destination
* @param departureTime the instant used to select the traffic timeframe matrix
* @param transportType the routing profile to use; {@code null} is treated as {@link TransportType#CAR}
* @return {@link TravelTime} instance representing the travel time in seconds.
* @throws IllegalArgumentException When the resolved matrix does not include both locations, or the resolver
* returns an out-of-bounds index.
* @throws IllegalStateException When no travel time matrix is configured for the given transport type.
*/
public TravelTime getTravelTimeTo(Location location, OffsetDateTime departureTime, TransportType transportType) {
DistanceMatrix matrix = travelTimeMatrixForMode(transportType, departureTime);
return TravelTime.of(lookup(matrix, location, transportType, "travel time", departureTime));
}

/**
* Returns the travel distance for a route between this location and the given location using the given transport
* type.
*
* @param location the location representing the route destination
* @param transportType the routing profile to use; {@code null} is treated as {@link TransportType#CAR}
* @return {@link TravelDistance} instance representing the travel distance in meters.
* @throws IllegalArgumentException When the resolved matrix does not include both locations.
* @throws IllegalStateException When no distance matrix is configured for the given transport type.
*/
public TravelDistance getDistanceTo(Location location, TransportType transportType) {
DistanceMatrix matrix = distanceMatrixForMode(transportType);

Check warning on line 335 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declare this local variable with "var" instead.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBrBq-ugWh1-ALLhBaD&open=AaBrBq-ugWh1-ALLhBaD&pullRequest=2579
return TravelDistance.of(lookup(matrix, location, transportType, "distance", null));
}

/**
* Returns the travel distance for a route between this location and the given location at the given departure
* time, using the given transport type.
*
* @param location the location representing the route destination
* @param departureTime the instant used to select the traffic timeframe matrix
* @param transportType the routing profile to use; {@code null} is treated as {@link TransportType#CAR}
* @return {@link TravelDistance} instance representing the travel distance in meters.
* @throws IllegalArgumentException When the resolved matrix does not include both locations, or the resolver
* returns an out-of-bounds index.
* @throws IllegalStateException When no distance matrix is configured for the given transport type.
*/
public TravelDistance getDistanceTo(Location location, OffsetDateTime departureTime, TransportType transportType) {
DistanceMatrix matrix = distanceMatrixForMode(transportType, departureTime);

Check warning on line 352 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declare this local variable with "var" instead.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBrBq-ugWh1-ALLhBaE&open=AaBrBq-ugWh1-ALLhBaE&pullRequest=2579
return TravelDistance.of(lookup(matrix, location, transportType, "distance", departureTime));
}

public short getIndex(DistanceMatrix matrix) {
if (matrix == travelTimeMatrix) {
return travelTimeMatrixIndex;
Expand Down Expand Up @@ -280,6 +407,72 @@
return matrix;
}

private static boolean isDefaultMode(TransportType transportType) {
return transportType == null || TransportType.CAR.equals(transportType);

Check warning on line 411 in service/maps/api/src/main/java/ai/timefold/solver/service/maps/api/model/Location.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "==" to perform this enum comparison instead of using "equals"

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBr1hBIp8EFLDUUZgJc&open=AaBr1hBIp8EFLDUUZgJc&pullRequest=2579
}

private DistanceMatrix travelTimeMatrixForMode(TransportType transportType) {
if (isDefaultMode(transportType)) {
return travelTimeMatrix;
}
return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode.get(transportType);
}

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.get(transportType);
if (byTimeframe != null && timeframeIndexResolver != null) {
return resolveTimeframeMatrix(byTimeframe, departureTime, "travel time");
}
return travelTimeMatrixByMode == null ? null : travelTimeMatrixByMode.get(transportType);
}

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

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

private long lookup(DistanceMatrix matrix, Location to, TransportType transportType, String what,
OffsetDateTime departureTime) {
TransportType resolvedMode = transportType == null ? TransportType.CAR : transportType;
if (matrix == null) {
throw new IllegalStateException(
"No %s matrix configured for a location (%s) and transport type (%s).".formatted(what, this,
resolvedMode));
}
long value = matrix.get(this, to);
if (value == -1) {
String at = departureTime == null ? "" : " at (%s)".formatted(departureTime);
throw new IllegalArgumentException(("No %s information found for a route from (%s) to (%s) for transport "
+ "type (%s)%s. Are both locations in the configured map and in the location set (if used)?")
.formatted(what, this, to, resolvedMode, at));
}
return value;
}

private void updateIndex(DistanceMatrix distanceMatrix) {
if (distanceMatrix instanceof IndexableDistanceMatrix indexableDistanceMatrix) {
indexableDistanceMatrix.updateCachedIndex(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ai.timefold.solver.service.maps.api.model;

import java.util.Objects;

import org.eclipse.microprofile.openapi.annotations.media.Schema;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

@Schema(description = "The type of transport used (car, bike, ... ) supported by Timefold.")
public enum TransportType {

CAR("car");

private final String value;

TransportType(String value) {
Objects.requireNonNull(value, "TransportType value must not be null.");
value = value.trim().toLowerCase();
if (value.isEmpty()) {
throw new IllegalArgumentException("TransportType value must not be blank.");
}
this.value = value;
}

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

@JsonValue
public String value() {
return value;
}

@Override
public String toString() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ai.timefold.solver.service.definition.internal.error.TimefoldRuntimeException;
import ai.timefold.solver.service.maps.api.DistanceMatrix;
import ai.timefold.solver.service.maps.api.model.Location;
import ai.timefold.solver.service.maps.api.model.TransportType;
import ai.timefold.solver.service.maps.service.client.api.model.TravelTimesByTimeframeWithMetadata;
import ai.timefold.solver.service.maps.service.client.impl.MapServiceOptionsSupplier;
import ai.timefold.solver.service.maps.service.client.impl.error.MapServiceIllegalArgumentException;
Expand Down Expand Up @@ -55,19 +56,30 @@
})
@Override
public LocationsAwareSolverModel<?> enrich(LocationsAwareSolverModel<?> solverModel) {
if (useTraffic) {
return enrichAllTimeframes(solverModel);
// One map-service round-trip per transport type; each mode resolves to its own OSRM instance. The first
// mode is treated as primary and is the one whose map metadata (locations-not-in-map, resolved location) is
// propagated to the solver model.
List<TransportType> transportTypes = optionsSupplier.getTransportTypes();
for (int i = 0; i < transportTypes.size(); i++) {

Check warning on line 63 in service/maps/service-client/src/main/java/ai/timefold/solver/service/maps/service/client/api/TravelTimeMatrixEnricher.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declare this local variable with "var" instead.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBrBq6fgWh1-ALLhBaB&open=AaBrBq6fgWh1-ALLhBaB&pullRequest=2579
TransportType transportType = transportTypes.get(i);

Check warning on line 64 in service/maps/service-client/src/main/java/ai/timefold/solver/service/maps/service/client/api/TravelTimeMatrixEnricher.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declare this local variable with "var" instead.

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AaBrBq6fgWh1-ALLhBaC&open=AaBrBq6fgWh1-ALLhBaC&pullRequest=2579
boolean primary = i == 0;
if (useTraffic) {
enrichAllTimeframes(solverModel, transportType, primary);
} else {
enrichSingleMatrix(solverModel, transportType, primary);
}
}
return enrichSingleMatrix(solverModel);
return solverModel;
}

private LocationsAwareSolverModel<?> enrichSingleMatrix(LocationsAwareSolverModel<?> solverModel) {
private void enrichSingleMatrix(LocationsAwareSolverModel<?> solverModel, TransportType transportType,
boolean primary) {
List<Location> locations = solverModel.getLocations(); // Get all the locations from the model only once.
TravelTimeAndDistanceWithMetadata travelTimeAndDistance;
try {
travelTimeAndDistance =
mapService.getTravelTimeAndDistance(locations,
optionsSupplier.getOptions(solverModel.getLocationSetName()));
optionsSupplier.getOptions(solverModel.getLocationSetName(), transportType));
} catch (TimefoldRuntimeException e) {
throw e;
} catch (Exception e) {
Expand All @@ -76,19 +88,22 @@
"Error getting travel time and distances from map service", e, false);
}
locations.forEach(location -> {
location.setTravelTimeMatrix(travelTimeAndDistance.travelTimeAndDistance().travelTime());
location.setDistanceMatrix(travelTimeAndDistance.travelTimeAndDistance().distance());
location.setTravelTimeMatrix(transportType, travelTimeAndDistance.travelTimeAndDistance().travelTime());
location.setDistanceMatrix(transportType, travelTimeAndDistance.travelTimeAndDistance().distance());
});
solverModel.setLocationsNotInMap(convertIdxToLocations(travelTimeAndDistance.locationsNotInMapIdx(), locations));
mapEnrichmentContext.setResolvedMapLocation(travelTimeAndDistance.resolvedMapLocation());
return solverModel;
if (primary) {
solverModel
.setLocationsNotInMap(convertIdxToLocations(travelTimeAndDistance.locationsNotInMapIdx(), locations));
mapEnrichmentContext.setResolvedMapLocation(travelTimeAndDistance.resolvedMapLocation());
}
}

private LocationsAwareSolverModel<?> enrichAllTimeframes(LocationsAwareSolverModel<?> solverModel) {
private void enrichAllTimeframes(LocationsAwareSolverModel<?> solverModel, TransportType transportType,
boolean primary) {
List<Location> locations = solverModel.getLocations();
TravelTimesByTimeframeWithMetadata result;
try {
result = mapService.getTravelTimeAndDistanceByTimeframe(locations, optionsSupplier.getOptions());
result = mapService.getTravelTimeAndDistanceByTimeframe(locations, optionsSupplier.getOptions(transportType));
} catch (TimefoldRuntimeException e) {
throw e;
} catch (Exception e) {
Expand All @@ -103,17 +118,18 @@
// IndexableDistanceMatrix index-cache fast path. The time-aware overloads keep working because Location
// falls back to the single matrix when no per-timeframe matrices are set.
for (Location location : locations) {
location.setTravelTimeMatrix(travelTimes[0]);
location.setDistanceMatrix(distances[0]);
location.setTravelTimeMatrix(transportType, travelTimes[0]);
location.setDistanceMatrix(transportType, distances[0]);
}
} else {
for (Location location : locations) {
location.setTravelTimeMatrices(travelTimes, result.timeframeIndexResolver());
location.setDistanceMatrices(distances, result.timeframeIndexResolver());
location.setTravelTimeMatrices(transportType, travelTimes, result.timeframeIndexResolver());
location.setDistanceMatrices(transportType, distances, result.timeframeIndexResolver());
}
}
solverModel.setLocationsNotInMap(result.locationsNotInMap());
return solverModel;
if (primary) {
solverModel.setLocationsNotInMap(result.locationsNotInMap());
}
}

@Override
Expand Down
Loading
Loading