Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
    • Pick-up and Delivery Routing
    • Task Scheduling
  • Platform
Start Free Trial
  • Timefold Solver SNAPSHOT
  • Build with Timefold
  • Running the Solver
  • As a service
  • Map service
  • Edit this Page

Timefold Solver SNAPSHOT

  • Getting started
    • Introduction
    • Overview
    • Build as a service
    • Embed as a library
      • Hello World guide
      • Quarkus guide
      • Spring Boot guide
  • Build with Timefold
    • Domain modeling
      • Guide
      • Building blocks
      • Common patterns
    • Constraints and score
      • Overview
      • Score calculation
      • Understanding the score
      • Load balancing and fairness
      • Performance tips and tricks
    • Running the Solver
      • Overview
      • As a service
        • REST API
        • Model configuration overrides
        • Model enrichment
        • Map service
        • Demo data
        • Exposing metrics
        • Service consumer guide
      • As a library
        • Configuring Timefold Solver
        • Constraint weights
        • Quarkus integration
        • Spring Boot integration
        • JPA/JAXB/JSON integration
    • Responding to change
      • Continuous planning
      • Real-time planning
      • Non-disruptive replanning
      • Assignment Recommendation API
    • Diagnosing the Solver
      • Benchmarking
      • Solver diagnostics
    • Use cases
      • Vehicle routing (guide)
      • More examples on GitHub
    • Optimization algorithms
      • Overview
      • Construction heuristics
      • Local search
      • Exhaustive search
    • Moves
      • Move Selector reference
      • Custom Moves
        • Move definition
        • Neighborhoods API
  • Deploy to Timefold Platform
    • Overview
    • Guide
    • Platform model metadata
    • Using metrics
  • Upgrading
    • New and noteworthy
    • Upgrade guides
      • Overview
      • Upgrade to latest
      • From 1.x to 2.x
      • From OptaPlanner
    • Feature migrations
      • Variable Listeners to Custom Shadow Variables
      • Chained planning variable to planning list variable
      • @ShadowVariablesInconsistent to structural scores
  • Enterprise Edition
    • Overview
    • Installation
    • Performance improvements
    • Score analysis
    • Recommendation API
    • Nearby selection
    • Multithreaded solving
    • Partitioned search
    • Constraint profiling
    • Multistage moves
    • Throttling best solution events
    • License management
  • Additional resources
    • FAQ
    • GitHub
Ask Claude Ask ChatGPT

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.

getTravelTimeTo(…​) and getDistanceTo(…​) only work once the matrix has been built and injected. When called on a Location that was never handed to the map extension, it throws. See Testing for how to satisfy this in unit tests, which bypass the model pipeline.

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

timefold.platform.map-service.use-remote

true

Use the remote map service of the Timefold Platform, which returns real road-network driving times and distances. Set to false to compute the matrix locally instead.

timefold.platform.map-service.enable-fallback

false

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 use-remote=false.

timefold.platform.map-service.provider

The map data provider to request the matrix from, for example haversine. See Map data providers.

timefold.platform.map-service.transport-type

The mode of transport the travel times are computed for.

timefold.platform.map-service.max-distance-from-road

How far a location may be from the nearest road before it is reported as not in the map.

timefold.platform.map-service.use-traffic

false

Take historical traffic into account, producing a travel time matrix per timeframe rather than a single one. Use getTravelTimeTo(destination, departureTime) and getDistanceTo(destination, departureTime) with an OffsetDateTime to select the timeframe for the departure time. The overloads without a departure time use the first available timeframe.

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.

use-remote=false and enable-fallback=true, as shown above, are development settings. They are what the vehicle-routing quickstart uses to run without any external dependency. In production, leave use-remote at its default so the solver optimizes against real travel times.

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

haversine

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 timefold.platform.map-service.use-remote=false; setting provider=haversine alone still uses the remote service by default. It ignores roads, water, one-way streets and traffic, so use it for development, demos and tests, not production planning. For real road data and traffic, use a road-network provider such as osrm below, available on the Timefold Platform.

osrm

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. With use-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 UNREACHABLE or throw an exception. Reject these locations, or exclude them from routing before constraints, shadow variables and nearby selection read the matrix.

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.

  • © 2026 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default