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

Timefold Solver SNAPSHOT

    • Introduction
    • Getting started
      • Overview
      • Build as a service
      • Embed as a library
        • Hello World guide
        • Quarkus guide
        • Spring Boot guide
    • 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
    • Diagnosing the Solver
      • Benchmarking
      • Solver diagnostics
    • Deploying to the Timefold Platform
      • Overview
      • Guide
      • Platform model metadata
      • Using metrics
    • Optimization algorithms
      • Overview
      • Construction heuristics
      • Local search
      • Exhaustive search
      • Custom moves
        • Neighborhoods API
        • Move Selector reference
    • Responding to change
      • Continuous planning
      • Real-time planning
      • Non-disruptive replanning
      • Assignment Recommendation API
    • Example use cases
      • Vehicle routing (guide)
      • More examples on GitHub
    • FAQ
    • New and noteworthy
    • Upgrading Timefold Solver
      • Upgrading Timefold Solver: Overview
      • Upgrade Timefold Solver to the latest version
      • Upgrade from Timefold Solver 1.x to 2.x
      • Upgrading from OptaPlanner
      • Backwards compatibility
      • Migration guides
        • Variable Listeners to Custom Shadow Variables
        • Chained planning variable to planning list variable
    • Commercial editions
      • Overview
      • Installation
      • Performance improvements
      • Score analysis
      • Recommendation API
      • Nearby selection
      • Multithreaded solving
      • Partitioned search
      • Constraint profiling
      • Multistage moves
      • Throttling best solution events
      • License management

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.

The Timefold ships a maps extension that does this for you. 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.getDrivingTimeTo(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.

A model whose locations are all in the map never sees this, which is why Locations not in the map matters: if you ignore the locations the map service could not resolve, the failure surfaces much later, as an exception in the middle of scoring.

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

2. Declaring the locations of your model

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, share a single Location instance per coordinate — LocationDeduplicator and UniqueLocationAccumulator in ai.timefold.solver.service.maps.api are there for exactly that — so the matrix stays as small as the set of distinct sites. Building a matrix is roughly quadratic in the number of locations, so this is not just a memory optimization.
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.

getLocations() is called on a model that may not be fully initialized yet. Guard against null collections so that the enricher sees an empty list rather than a NullPointerException.

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) {
        return origin.getLocation().getDrivingTimeTo(destination.getLocation()).seconds();
    }
}
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 platform’s remote map service, 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 the local computation when the remote map service is disabled or unavailable, instead of failing the solve.

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.

The local computation is a great-circle (Haversine) distance converted to a driving 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 entirely, so never benchmark solution quality against it and then expect the same numbers in production.

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 that the solver optimizes against real driving 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. No map data and no network access, so it is instant and always available — but it ignores roads, water and one-way streets entirely. Suitable for development, demos and tests; never for production planning.

TODO: add more

Road-network-aware providers, and the ability to plug in your own, are part of Timefold Solver Enterprise Edition — see Bringing your own matrices.

6.1. Bringing your own matrices

Not every organization wants a map service to compute driving times. Some already have a routing engine, a negotiated contract with a map data vendor, or a matrix derived from their own historical GPS traces.

For those cases, Timefold Solver Enterprise Edition lets you supply a custom provider: an implementation that hands the map extension a matrix you computed yourself. From the solver model’s point of view nothing changes. Location.getDrivingTimeTo(…​) still answers from the matrix, and the model never learns where the numbers came from.

A provider implements a small contract: an identifier it is selected by, one method that computes the full matrix over a list of locations, one that computes a rectangular origins-by-destinations matrix, and one that reports which locations it could not place on its map — those become the model’s getLocationsNotInMap(), see Locations not in the map.

This feature is exclusive to Timefold Solver Enterprise Edition.

TODO: How to?

7. One-off matrices versus named location sets

getLocationSetName() decides how the matrix is obtained:

Empty (Optional.empty())

Every solve builds its own one-off matrix from the locations the model returns. This is the simplest option and the right one 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 extension and reused across solves. Building a large matrix is expensive, it grows with the square of the number of locations, so a fleet that visits the same few thousand sites every day should build the matrix once and name it, rather than rebuild it on every run.

QUESTION: Does the Naming also work with a custom distance matrix?

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. TODO: REF TO THOSE DOCS

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 that names a set which is not yet COMPLETED cannot use it to enrich locations; make sure the set has finished processing before submitting solves that depend on it.

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.

What you must not do is ignore the list. Travel times involving those locations are UNREACHABLE, and the first seconds() call on one (e.g. in a constraint, a shadow variable supplier or the output conversion) throws IllegalStateException, aborting the solve with an error that points at the scoring code rather than at the bad input that caused it.

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 getDrivingTimeTo(…​) fails.

Build the matrix yourself in the test fixture, using the same Haversine provider the platform uses for its local computation:

import ai.timefold.solver.service.maps.haversine.impl.HaversineTravelTimeAndDistanceMatrixProvider;
import ai.timefold.solver.service.maps.service.test.api.TestDistanceCalculator;

private static final HaversineTravelTimeAndDistanceMatrixProvider PROVIDER =
        new HaversineTravelTimeAndDistanceMatrixProvider(new ObjectMapper());

public static VehicleRoutePlan initDistanceMap(VehicleRoutePlan plan) {
    TestDistanceCalculator.initDistanceMaps(plan.getLocations(), (1)
            PROVIDER::calculateDistance,
            PROVIDER::calculateTravelTime);
    return plan;
}
1 The same getLocations() the real map extension would be given, so the test covers exactly the locations the production matrix would.

This also gives tests a way to derive their expected travel times 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