Map service
A routing model needs to know how long it takes to drive from one location to another. Computing that yourself means either a crude straight-line approximation, or building and maintaining an integration with a road-network routing engine.
Timefold Solver ships a maps extension that does this for you. On its own it computes approximate times from straight-line distances. Connected to the Timefold Platform, it uses real road distances and traffic data. Your solver model declares which locations it uses; before every solve, the maps extension builds a travel time and distance matrix covering those locations and injects it into them. Your domain classes then simply ask a location how far away another location is.
The vehicle-routing quickstart uses the map
service exactly this way, and is used as the running example throughout this page.
1. The Location type
Every point on the map is an ai.timefold.solver.service.maps.api.model.Location, constructed from
a latitude and a longitude:
import ai.timefold.solver.service.maps.api.model.Location;
Location location = new Location(51.01, 3.66); // latitude, longitude
A Location is more than a coordinate pair: once the map extension has enriched it, it carries a
reference into the travel time and distance matrix, so it can answer questions about other
locations in that same matrix:
TravelTime drivingTime = origin.getTravelTimeTo(destination);
TravelDistance distance = origin.getDistanceTo(destination);
long seconds = drivingTime.seconds();
long meters = distance.meters();
TravelTime and TravelDistance are records with an explicit notion of reachability.
A pair of locations that the map service could not connect by road yields
TravelTime.UNREACHABLE rather than a silently wrong number:
if (!drivingTime.isReachable()) {
// No road connection between these two locations.
}
This is a fail-fast type: seconds() on an unreachable TravelTime throws
IllegalStateException ("Cannot retrieve an unreachable TravelTime value."), and meters() on an
unreachable TravelDistance does the same.
There is no silent sentinel value that could quietly end up in a score.
Where a zero is the acceptable answer for an unreachable pair, use reachableSeconds() /
reachableMeters(), which return 0 instead of throwing.
Even when every location is in the map, two locations can be on disconnected road networks.
Check isReachable() before calling seconds() or meters() on a route value.
Also handle the unresolved locations described in Locations not in the map.
|
|
2. Declaring the locations of your model
Add the maps integration dependency to your project’s pom.xml:
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-service-with-maps</artifactId>
</dependency>
There is no separate artifact for the Enterprise providers or the Timefold Platform. Moving from local computation to real road data is a configuration change, see Configuration and Map data providers.
To let the map extension build the matrix, your @PlanningSolution implements
LocationsAwareSolverModel<Score_> instead of the plain SolverModel<Score_>:
@PlanningSolution
public class VehicleRoutePlan implements LocationsAwareSolverModel<HardMediumSoftScore> {
@PlanningEntityCollectionProperty
private List<Vehicle> vehicles;
@PlanningEntityCollectionProperty
@ValueRangeProvider
private List<Visit> visits;
private List<Location> locationsNotInMap = List.of();
@Override
public List<Location> getLocations() { (1)
if (vehicles == null || visits == null) {
return List.of();
}
return Stream.concat(
vehicles.stream().map(Vehicle::getHomeLocation),
visits.stream().map(Visit::getLocation))
.toList();
}
@Override
public Optional<String> getLocationSetName() { (2)
return Optional.empty();
}
@Override
public void setLocationsNotInMap(List<Location> locationsNotInMap) { (3)
this.locationsNotInMap = locationsNotInMap;
}
@Override
public List<Location> getLocationsNotInMap() {
return locationsNotInMap;
}
// ...
}
| 1 | Every location the matrix must cover: each vehicle’s home location and each visit’s location.
Return them in any order.
Nothing deduplicates this list for you: each Location instance you return gets enriched (and
counted) individually, even if two of them share the exact same coordinates.
If your input can contain the same coordinate many times, use LocationDeduplicator.same(…)
when constructing domain objects so that they share one Location instance per coordinate.
Return the deduplicator’s asList() from getLocations(), or collect those shared instances with
UniqueLocationAccumulator, so each location appears only once in the returned list.
Sharing instances alone does not remove repeated list entries;
the local provider still computes every pair of entries in that list. |
| 2 | The name of a reusable location set, or empty to build a one-off matrix for this solve. See One-off matrices versus named location sets. |
| 3 | The map service calls this back with the locations it could not resolve onto the road network, so the model keeps that information instead of silently dropping it. Report those back to the end user, or reject the input. |
Under the hood, the platform enriches the model before the solver starts: it calls
getLocations(), requests the matrix from the map service, and injects it into each Location.
No code in your model calls the map service directly.
This follows the same shape as any other model
enrichment. The map service is simply a built-in enricher the platform provides for you.
3. Using driving times in the domain
Because the matrix lives inside the Location objects, the rest of the domain model reads
naturally. If you have multiple planning entities or values with a location, you typically want to create a shared interface.
This makes them interchangeable wherever only a location matters:
public interface LocationAware {
Location getLocation();
}
4. Nearby selection
| This feature is exclusive to Timefold Solver Enterprise Edition. |
Driving time is also the natural distance measure for
nearby selection.
Implement NearbyDistanceMeter on top of the same Location calls:
public class LocationDistanceMeter implements NearbyDistanceMeter<Visit, LocationAware> {
@Override
public double getNearbyDistance(Visit origin, LocationAware destination) {
var travelTime = origin.getLocation().getTravelTimeTo(destination.getLocation());
return travelTime.isReachable() ? travelTime.seconds() : Double.MAX_VALUE;
}
}
quarkus.timefold.solver.nearby-distance-meter-class=org.acme.vehiclerouting.domain.LocationDistanceMeter
The destination type is LocationAware rather than Visit, so that a move towards a vehicle’s
home location is measured the same way as a move towards another visit.
5. Configuration
The map extension is configured through application.properties, under the
timefold.platform.map-service prefix:
timefold.platform.map-service.use-remote=false
timefold.platform.map-service.enable-fallback=true
| Property | Default | Description |
|---|---|---|
|
|
Use the remote map service of the Timefold Platform, which returns real road-network driving times and
distances. Set to |
|
|
Fall back to local computation when a remote request fails after application startup.
This does not bypass the startup availability check and has no effect when |
|
The map data provider to request the matrix from, for example |
|
|
The mode of transport the travel times are computed for. |
|
|
How far a location may be from the nearest road before it is reported as not in the map. |
|
|
|
Take historical traffic into account, producing a travel time matrix per timeframe rather than a
single one.
Use |
When use-remote=true (the default), the extension also needs the REST client settings for the
remote map service it connects to:
# Map service endpoint
timefold.platform.map-service.url=http://localhost:8180
quarkus.rest-client.map-service.url=${timefold.platform.map-service.url}
# Distance matrix calculation potentially takes a long time, avoid timeouts
timefold.platform.map-service.read-timeout=3600000
quarkus.rest-client.map-service.read-timeout=${timefold.platform.map-service.read-timeout}
The remote map service is not packaged with Timefold Solver, it is only available on the Timefold Platform.
The local computation is a great-circle (Haversine) distance converted to a travel time at a fixed average speed. It requires no network access and no map data, which makes it the right choice for development, demos and tests, but the times it produces ignore roads and traffic entirely. Only the Timefold Platform brings actual road distances and traffic data. Never benchmark solution quality against Haversine and then expect the same numbers in production with real map data.
|
|
6. Map data providers
A provider is the thing that actually answers "how long does it take to drive from A to B". The map extension is not tied to one: it selects a provider by name, so the same solver model can be run against a crude approximation in development and a real road-network engine in production without a line of code changing.
timefold.platform.map-service.provider=haversine
| Provider | Availability | Description |
|---|---|---|
|
Open source |
Great-circle distance between the two coordinates, converted to a driving time at a fixed average speed.
For network-free local computation, set |
|
Enterprise |
Driving time and distance computed from real road-network data by an OSRM instance. Follows roads, water and one-way streets, so it is accurate enough for production planning, but it requires map data to be loaded into the OSRM instance and network access to reach it. |
Any provider other than haversine runs on the remote map service, so it also needs the endpoint
settings described in Configuration.
7. One-off matrices versus named location sets
When use-traffic=true, the enricher does not use the location set name.
It requests matrices for the model’s locations for every timeframe instead.
With use-traffic=false, getLocationSetName() decides how the matrix is obtained:
- Empty (
Optional.empty()) -
Every solve gets a matrix for the locations the model returns. When
use-remote=true, the client keeps the most recent matrix in memory, keyed by the set of locations and the routing options. A solve that returns the same locations with the same options reuses it instead of requesting it again. Withuse-remote=false, the local provider computes the matrix again for every solve. This is the simplest option, and it is enough when you solve the same locations repeatedly within one running instance, for example when re-solving after a small change to the constraints. It is also the right choice when the locations differ from one dataset to the next, as they do in the vehicle routing quickstart. - A name (Platform only)
-
A matrix can be stored under a name in the map service and reused across solves. The in-memory cache holds only one matrix, does not survive a restart, and is not shared between instances. Any change to the set of locations also forces a new matrix. Building a large matrix is expensive: it holds one entry for every pair of locations, so 100 locations need 10,000 entries and 1,000 locations need 1,000,000. A fleet that visits the same few thousand sites every day, across several instances or restarts, should build the matrix once and name it. A name also lets the matrix be updated when locations change, instead of being rebuilt in full.
Creating and populating a named location set is a management operation on the map service, not something your solver model does. Consult the Timefold Platform documentation for how to create one.
Once created, a named location set has one of three states: PROCESSING while the map service
builds it, COMPLETED once it is ready to be used by a solve, or NOT_FOUND if the name a solve
refers to was never created (or was cleared).
A solve can reuse a named matrix only after the set is COMPLETED.
If the set does not exist or has been deleted, the client falls back to requesting a matrix for the model’s locations.
8. Locations not in the map
When running with a real map provider, it could happen that a coordinate could not be placed on a road network: a typo in a latitude, a site in the middle of
a lake, or a location beyond the max-distance-from-road threshold.
Rather than failing the whole solve, the map extension reports these back through
setLocationsNotInMap(…).
Decide explicitly what that means for your model. Typically one of two actions:
-
Reject the input with an actionable error message naming the offending locations.
-
Let the affected visits stay unassigned and surface them in the output.
|
You must not ignore the list.
Travel times and distances involving these locations must not be used.
A lookup is not guaranteed to return |
9. Testing
ConstraintVerifier tests construct planning entities directly, bypassing the model conversion
pipeline the map extension hooks into.
The Location objects in such a test therefore have no matrix, and the first call to
getTravelTimeTo(…) fails.
Build the matrix yourself in the test fixture with TestDistanceCalculator.initDistanceMaps(…).
It takes the locations to cover, a distance function and a travel time function.
Any function works: the matrix does not have to come from a real map.
For constraint tests, fixed values are usually the clearest choice, because the expected penalties are easy to verify by hand:
class VehicleRoutingConstraintProviderTest {
private static final Location LOCATION_1 = new Location(51.01, 3.66);
private static final Location LOCATION_2 = new Location(51.02, 3.68);
private static final Location DISTANT_LOCATION = new Location(50.85, 4.35);
@BeforeAll
static void initDistanceMaps() {
TestDistanceCalculator.initDistanceMaps(List.of(LOCATION_1, LOCATION_2, DISTANT_LOCATION),
(Location from, Location to) -> fixedValue(from, to), // distance function
(Location from, Location to) -> fixedValue(from, to)); // travel time function, in seconds
}
private static long fixedValue(Location from, Location to) {
if (from.equals(to)) {
return 0;
}
return from.equals(DISTANT_LOCATION) || to.equals(DISTANT_LOCATION) ? 3600 : 1;
}
// ... tests
}
TestDistanceCalculator lives in ai.timefold.solver.service.maps.service.test.api.
To test with more realistic values instead, use the Haversine provider and pass in all the locations in the PlanningSolution:
// HaversineTravelTimeAndDistanceMatrixProvider is in ai.timefold.solver.service.maps.haversine.impl
private static final HaversineTravelTimeAndDistanceMatrixProvider PROVIDER =
new HaversineTravelTimeAndDistanceMatrixProvider(new ObjectMapper());
public static VehicleRoutePlan initDistanceMap(VehicleRoutePlan plan) {
TestDistanceCalculator.initDistanceMaps(plan.getLocations(),
PROVIDER::calculateDistance,
PROVIDER::calculateTravelTime);
return plan;
}
In that case, derive the expected travel times from the provider instead of hard-coding magic numbers:
long expectedSeconds = PROVIDER.calculateTravelTime(
new Location(51.01, 3.66), new Location(51.02, 3.68));
Add the test support artifact for TestDistanceCalculator:
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-service-maps-service-test</artifactId>
<scope>test</scope>
</dependency>
Integration tests that go through the REST resource need none of this: they exercise the full
model pipeline, so the enricher builds the matrix for them, against whichever map provider the
timefold.platform.map-service.* properties of the test profile select.