Skip to content
Open
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
Expand Up @@ -4,13 +4,35 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;

@JsonInclude(Include.NON_NULL)
public record MapsConfiguration(String provider, String location, Double maxDistanceFromRoad, String transportType) {
public record MapsConfiguration(String provider, String location, Double maxDistanceFromRoad, String transportType,
Boolean useTraffic) {

public MapsConfiguration(String provider) {
this(provider, null, null, null, null);
}

public MapsConfiguration withLocation(String location) {
return new MapsConfiguration(provider, location, maxDistanceFromRoad, transportType, useTraffic);
}

public MapsConfiguration withMaxDistanceFromRoad(Double maxDistanceFromRoad) {
return new MapsConfiguration(provider, location, maxDistanceFromRoad, transportType, useTraffic);
}

public MapsConfiguration withTransportType(String transportType) {
return new MapsConfiguration(provider, location, maxDistanceFromRoad, transportType, useTraffic);
}

public MapsConfiguration withUseTraffic(Boolean useTraffic) {
return new MapsConfiguration(provider, location, maxDistanceFromRoad, transportType, useTraffic);
}

public MapsConfiguration override(MapsConfiguration configuration) {
String finalLocation = location;
String finalProvider = provider;
Double finalMaxDistanceFromRoad = maxDistanceFromRoad;
String finalTransportType = transportType;
Boolean finalUseTraffic = useTraffic;

if (configuration == null) {
return this;
Expand All @@ -28,8 +50,12 @@ public MapsConfiguration override(MapsConfiguration configuration) {
if (transportType == null) {
finalTransportType = configuration.transportType();
}
if (useTraffic == null) {
finalUseTraffic = configuration.useTraffic();
}

return new MapsConfiguration(finalProvider, finalLocation, finalMaxDistanceFromRoad, finalTransportType);
return new MapsConfiguration(finalProvider, finalLocation, finalMaxDistanceFromRoad, finalTransportType,
finalUseTraffic);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ai.timefold.solver.service.maps.api;

import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ai.timefold.solver.service.maps.api.model.Location;
import ai.timefold.solver.service.maps.api.model.TimeInterval;

/**
* Accumulates {@link Location}s with their time availability data while checking for duplicates.
*/
public final class TimeAwareUniqueLocationAccumulator {
private final Set<Location> instanceUniqueLocations = Collections.newSetFromMap(new IdentityHashMap<>());
private final Set<Location> equalityUniqueLocations = new LinkedHashSet<>();
private final Map<Location, List<TimeInterval>> locationsWithAvailability = new LinkedHashMap<>();

/**
* Adds a single {@link Location} while checking for duplicates.
* <p>
* For each unique location, the time intervals when the location is available for traveling are merged.
*
* @return true if the accumulator already contains a different equal instance of this location, otherwise false
*/
public boolean addLocation(Location location, List<TimeInterval> availableTimes) {
boolean addedEquality = equalityUniqueLocations.add(location);
boolean addedInstance = instanceUniqueLocations.add(location);
locationsWithAvailability.computeIfAbsent(location, l -> new ArrayList<>()).addAll(availableTimes);
// There are two equal instances, which means that locations have not been deduplicated.
return !addedEquality && addedInstance;
}

/**
* Retrieves a map of locations and their corresponding availability time intervals.
* Each location is associated with a list of time intervals during which it is available for traveling.
*
* @return a map where the keys are {@link Location} instances and the values are lists of {@link TimeInterval} instances
* representing the periods during which the locations are available.
*/
public Map<Location, List<TimeInterval>> getLocationsWithAvailability() {
return locationsWithAvailability;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package ai.timefold.solver.service.maps.api.model;

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

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;

Expand Down Expand Up @@ -38,6 +41,15 @@ public class Location {
@JsonIgnore
private short distanceMatrixIndex = IndexableDistanceMatrix.EMPTY_INDEX;

@JsonIgnore
private DistanceMatrix[] travelTimesByTimeframe;

@JsonIgnore
private DistanceMatrix[] distancesByTimeframe;

@JsonIgnore
private ToIntFunction<OffsetDateTime> timeframeIndexResolver;

public Location() {
}

Expand All @@ -50,6 +62,10 @@ public Location(@JsonProperty("latitude") @Min(-90) @Max(90) double latitude,
validateLongitude();
}

public static Location of(double latitude, double longitude) {
return new Location(latitude, longitude);
}

public double getLatitude() {
return latitude;
}
Expand Down Expand Up @@ -80,6 +96,18 @@ public void setDistanceMatrix(DistanceMatrix distanceMatrix) {
updateIndex(this.distanceMatrix);
}

public void setTravelTimeMatrices(DistanceMatrix[] travelTimesByTimeframe,
ToIntFunction<OffsetDateTime> indexResolver) {
this.travelTimesByTimeframe = travelTimesByTimeframe;
this.timeframeIndexResolver = indexResolver;
}

public void setDistanceMatrices(DistanceMatrix[] distancesByTimeframe,
ToIntFunction<OffsetDateTime> indexResolver) {
this.distancesByTimeframe = distancesByTimeframe;
this.timeframeIndexResolver = indexResolver;
}

/**
* Returns the travel time for a route between this location and the given location.
*
Expand All @@ -88,20 +116,50 @@ public void setDistanceMatrix(DistanceMatrix distanceMatrix) {
* unreachable from this location.
* @throws IllegalArgumentException When both locations are not included in the travel time matrix (either missing from the
* map or from the pre-configured location set).
* @throws IllegalStateException When there is no travel time matrix configured for this location.
* @throws IllegalStateException When there is neither a single nor a per-timeframe travel time matrix configured for
* this location.
*/
public TravelTime getTravelTimeTo(Location location) {
if (travelTimeMatrix == null) {
DistanceMatrix matrix = travelTimeMatrix != null ? travelTimeMatrix : firstAvailableMatrix(travelTimesByTimeframe);
if (matrix == null) {
throw new IllegalStateException("No travel time matrix configured for a location (%s).".formatted(this));
}
long travelTimeFromMatrix = travelTimeMatrix.get(this, location);
long travelTimeFromMatrix = matrix.get(this, location);
if (travelTimeFromMatrix == -1) {
throw new IllegalArgumentException(("No travel time information found for a route from (%s) to (%s). " +
"Are both locations in the configured map and in the location set (if used)?").formatted(this, location));
}
return TravelTime.of(travelTimeFromMatrix);
}

/**
* Returns the travel time for a route between this location and the given location at the given departure time.
*
* @param location the location representing the route destination
* @param departureTime the instant used to select the traffic timeframe matrix
* @return {@link TravelTime} instance representing the travel time in seconds and indicating if the destination is
* unreachable from this location.
* @throws IllegalArgumentException When the resolved matrix does not include both locations, or the resolver returns
* an out-of-bounds index.
* @throws IllegalStateException When there is neither a per-timeframe nor a single travel time matrix configured for
* this location.
*/
public TravelTime getTravelTimeTo(Location location, OffsetDateTime departureTime) {
DistanceMatrix matrix = hasTimeframeMatrices(travelTimesByTimeframe)
? resolveTimeframeMatrix(travelTimesByTimeframe, departureTime, "travel time")
: travelTimeMatrix;
if (matrix == null) {
throw new IllegalStateException("No travel time matrix configured for a location (%s).".formatted(this));
}
long travelTime = matrix.get(this, location);
if (travelTime == -1) {
throw new IllegalArgumentException(
("No travel time information found for a route from (%s) to (%s) at (%s).")
.formatted(this, location, departureTime));
}
return TravelTime.of(travelTime);
}

@Deprecated
public TravelTime getDrivingTimeTo(Location location) {
return getTravelTimeTo(location);
Expand All @@ -115,20 +173,49 @@ public TravelTime getDrivingTimeTo(Location location) {
* unreachable from this location.
* @throws IllegalArgumentException When both locations are not included in the travel distance matrix (either missing from
* the map or from the pre-configured location set).
* @throws IllegalStateException When there is no travel distance matrix configured for this location.
* @throws IllegalStateException When there is neither a single nor a per-timeframe distance matrix configured for this
* location.
*/
public TravelDistance getDistanceTo(Location location) {
if (distanceMatrix == null) {
DistanceMatrix matrix = distanceMatrix != null ? distanceMatrix : firstAvailableMatrix(distancesByTimeframe);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I suppose the firstAvailableMatrix is there for the situation when traffic data was enabled, but we don't ask for it, correct?

Instead of finding the first available matrix on the hot path, why don't we select the right one and set it as the distanceMatrix reference during enrichment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's correct. We don't do that because we don't know which is the "right" one, that's why we just take the first. We can do this in the enricher and set the first matrix as the "single" matrix, although it's a tradeoff: we get the optimizations for single matrix, but the object will take more memory (3 matrices + 1 repeated matrix for requests without timestamp).

if (matrix == null) {
throw new IllegalStateException("No distance matrix configured for a location (%s).".formatted(this));
}
long travelDistanceFromMatrix = distanceMatrix.get(this, location);
long travelDistanceFromMatrix = matrix.get(this, location);
if (travelDistanceFromMatrix == -1) {
throw new IllegalArgumentException(("No travel distance information found for a route from (%s) to (%s). " +
"Are both locations in the configured map and in the location set (if used)?").formatted(this, location));
}
return TravelDistance.of(travelDistanceFromMatrix);
}

/**
* Returns the travel distance for a route between this location and the given location at the given departure time.
*
* @param location the location representing the route destination
* @param departureTime the instant used to select the traffic timeframe matrix
* @return {@link TravelDistance} instance representing the travel distance in meters and indicating if the destination is
* unreachable from this location.
* @throws IllegalArgumentException When the resolved matrix does not include both locations, or the resolver returns
* an out-of-bounds index.
* @throws IllegalStateException When there is neither a per-timeframe nor a single distance matrix configured for this
* location.
*/
public TravelDistance getDistanceTo(Location location, OffsetDateTime departureTime) {
DistanceMatrix matrix = hasTimeframeMatrices(distancesByTimeframe)
? resolveTimeframeMatrix(distancesByTimeframe, departureTime, "distance")
: distanceMatrix;
if (matrix == null) {
throw new IllegalStateException("No distance matrix configured for a location (%s).".formatted(this));
}
long distance = matrix.get(this, location);
if (distance == -1) {
throw new IllegalArgumentException(("No distance information found for a route from (%s) to (%s) at (%s).")
.formatted(this, location, departureTime));
}
return TravelDistance.of(distance);
}

public short getIndex(DistanceMatrix matrix) {
if (matrix == travelTimeMatrix) {
return travelTimeMatrixIndex;
Expand All @@ -147,6 +234,42 @@ public void setIndex(DistanceMatrix matrix, short index) {
}
}

private boolean hasTimeframeMatrices(DistanceMatrix[] matrices) {
return matrices != null && timeframeIndexResolver != null;
}

private static DistanceMatrix firstAvailableMatrix(DistanceMatrix[] matrices) {
if (matrices == null) {
return null;
}
for (DistanceMatrix matrix : matrices) {
if (matrix != null) {
return matrix;
}
}
return null;
}

private DistanceMatrix resolveTimeframeMatrix(DistanceMatrix[] matrices, OffsetDateTime departureTime, String what) {
if (matrices == null || timeframeIndexResolver == null) {
throw new IllegalStateException(
("No traffic-aware %s matrices configured for a location (%s).").formatted(what, this));
}
int index = timeframeIndexResolver.applyAsInt(departureTime);
if (index < 0 || index >= matrices.length) {
throw new IllegalArgumentException(
("Resolved timeframe index %d is out of bounds for %d %s matrix/matrices on location (%s) at (%s).")
.formatted(index, matrices.length, what, this, departureTime));
}
DistanceMatrix matrix = matrices[index];
if (matrix == null) {
throw new IllegalArgumentException(
("No %s matrix fetched for timeframe index %d on location (%s) at (%s).")
.formatted(what, index, this, departureTime));
}
return matrix;
}

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,22 @@
package ai.timefold.solver.service.maps.api.model;

import java.time.OffsetDateTime;
import java.util.Objects;

/**
* A half-open time interval {@code [from, to)} representing the period during which a location may be involved in
* travel. {@link #from()} is inclusive; {@link #to()} is exclusive. An interval where {@code from.equals(to)} is a
* zero-length (empty) interval — it still carries the meaning that the location is relevant at exactly {@code from}.
*/
public record TimeInterval(OffsetDateTime from, OffsetDateTime to) {

public TimeInterval {
Objects.requireNonNull(from, "from");
Objects.requireNonNull(to, "to");
if (from.isAfter(to)) {
throw new IllegalArgumentException(
"from (%s) must not be after to (%s)".formatted(from, to));
}
}

}
Loading
Loading