Custom distances and driving times with a node connector
This page comes from Java-TourOptimizer-Examples, last changed in commit 4734793.
A node connector gives JOpt the distance and the driving time between two elements from your own source, and JOpt fills in every pair that you leave out. In these examples, a connection of 5 km between Koeln and Oberhausen, two cities about 60 km apart in a straight line, makes the given values easy to find in the result. The page is for developers who bring in values from their own routing source. If you have not run an example yet, start with the First optimization example.
What the example shows
Two classes show the node connector:
- ExternalNodeConnectionExample.java sets one connection in each direction between Koeln and Oberhausen. All other pairs of elements get their values from the default backup connector.
- ExternalNodeConnectionWithLocationIdExample.java does the same with location ids: Koeln and Koeln1 share a location id, so the connection given for Koeln also serves Koeln1.
Why external connections matter
For many problems, JOpt's default backup connector is good enough: you provide nodes/resources with geo-coordinates and time windows and the optimizer can infer missing connections. However, in real routing use-cases you often need:
- Road network distances instead of straight-line distances
- Travel times depending on traffic, vehicle type, terrain, or service policies
- A precomputed matrix coming from:
- OSRM / Valhalla / GraphHopper,
- Google Distance Matrix,
- internal GIS systems,
- company-specific routing engines.
In those cases, you can inject external edge data by defining explicit connections for selected (or all) pairs of elements. For example, provide external edges for depot to customer and customer to depot (one item for each direction).
ExternalNodeConnectionWithLocationIdExample
Why you would use location ids
Location ids are a practical tool when your "logical nodes" are not all distinct physical points, for example:
- multiple tasks at the same customer site,
- multiple deliveries at one facility,
- clustered tasks that should share travel-time semantics.
In addition, location ids can reduce the number of distinct "places" your external matrix must cover (depending on how you structure your integration), because multiple nodes map to one physical location identity.
When to be careful
locationId is a powerful concept to represent multiple tasks at the same physical position; use it carefully and consistently. If you assign the same locationId to nodes that do not share the same latitude/longitude, you create ambiguity:
- explicit connections might be applied unexpectedly,
- fallback distances might be computed from an inconsistent geo position.
The class comment of the example warns about this, and the example only shares a location id between nodes with identical coordinates.
Scenario
Both classes use the same data:
- Nodes: eight nodes in Germany: Koeln, Koeln1, Oberhausen, Essen, Heilbronn, Stuttgart, Wuppertal and Aachen. Koeln and Koeln1 have the same coordinates. Every node is open on March 6 and 7, 2020, from 08:00 to 17:00, a visit takes 20 minutes, and all nodes have the same importance.
- Resources: Jack and John start in Aachen. Both work on March 6 from 08:00 to 17:00 and on March 7 from 08:00 to 20:00. They differ only in their maximal working time: 8 hours for Jack and 14 hours for John. The maximal distance of 1200 km does not limit any route of the example.
- Connections: one connection in each direction between Koeln and Oberhausen, with 5 km and 10 minutes. The connection is artificial on purpose: in a straight line the two cities are about 60 km apart, so the given values are easy to spot in the result.
- Time zone and data: Europe/Berlin. The dates, the coordinates and the connection values are sample values. The examples set no optimization properties.
How the code works
The code is in ExternalNodeConnectionExample.java and ExternalNodeConnectionWithLocationIdExample.java.
The steps
example() of ExternalNodeConnectionExample:
public void example()
throws InterruptedException, ExecutionException, TimeoutException, InvalidLicenceException,
IOException {
// The public evaluation license, or your own license if you put a license file where
// ExampleLicenseHelper looks for it.
ExampleLicenseHelper.setLicense(this);
addNodes(this);
addResources(this);
attachToObservables(this);
IOptimizationResult result = startAndPresentResult(this);
exportToKml(result);
}
addNodes adds the eight nodes and then calls addNodeConnections with the nodes Koeln and Oberhausen. ExternalNodeConnectionWithLocationIdExample has the same steps without the KML file.
Connections are directed edges
Use NodeEdgeConnectorItem to inject external distance and driving time data. A NodeEdgeConnectorItem (via INodeConnectorItem) represents:
- a directed edge from element A to element B
- distance (unit-aware quantity)
- driving time (
Duration)
Connections are directed: an item from A to B says nothing about B to A. Define both directions when you need them, one item per direction, also for the same values both ways, as addNodeConnections does.
The connection items
addNodeConnections creates one item for each direction. Each item names the from element and the to element, the distance as a quantity and the driving time as a Duration. The method then puts both items into a NodeEdgeConnector and hands the connector to the optimization with setNodeConnector:
private static void addNodeConnections(IOptimization opti, INode koeln, INode oberhausen) {
// Artificial on purpose: the straight line is about 60 km, so the given edge is easy to spot.
Quantity<Length> distance = Quantities.getQuantity(5, KILO(METRE));
Duration drivingTime = Duration.ofMinutes(10);
INodeConnectorItem koelnToOberhausen = new NodeEdgeConnectorItem();
koelnToOberhausen.setFromOptimizationElement(koeln);
koelnToOberhausen.setToOptimizationElement(oberhausen);
koelnToOberhausen.setDistance(distance);
koelnToOberhausen.setDrivingTime(drivingTime);
// A connection has a direction, so the way back needs an item of its own.
INodeConnectorItem oberhausenToKoeln = new NodeEdgeConnectorItem();
oberhausenToKoeln.setFromOptimizationElement(oberhausen);
oberhausenToKoeln.setToOptimizationElement(koeln);
oberhausenToKoeln.setDistance(distance);
oberhausenToKoeln.setDrivingTime(drivingTime);
// Every other pair gets its values from the default backup connector: the straight-line
// distance of a flat-earth approximation, and the driving time at the average speed of the
// resource (setAvgSpeed).
INodeEdgeConnector connector = new NodeEdgeConnector();
connector.putNodeConnections(List.of(koelnToOberhausen, oberhausenToKoeln));
opti.setNodeConnector(connector);
}
Before you hand external edges to the connector, verify every one of them: check that distance and driving time are not negative, that the units are right and that from and to point the right way.
The default backup connector
When you do not supply all connections, fallback kicks in: missing connections fall back to auto-calculated values. A NodeEdgeConnector created with new NodeEdgeConnector() uses a DefaultFlatEarthAverageSpeedBackupElementConnector, an implementation of IBackupElementConnector. For every pair without an item, it calculates the straight-line distance of a flat-earth approximation from the coordinates of the two elements, and the driving time from that distance and the average speed of the resource. The average speed is a property of the resource, set with setAvgSpeed. Both classes set an average speed of 22 m/s on each resource. This fallback can be customized via BackupConnector: to replace the straight-line fallback, pass your own IBackupElementConnector to NodeEdgeConnector(IBackupElementConnector); the BackupConnector feature guide shows how.
The BackupConnector documentation describes:
- Default distance calculation: a flat-surface earth approximation (efficient; usually accurate enough for short edges in optimized tours).
- Default driving time: derived from an assumed average speed (the docs mention a default average speed and show how to set a per-resource average speed).
- How to override/adjust the calculation by providing a custom
IBackupElementConnectorimplementation.
Practical recommendation:
- If you only provide a partial external matrix, consider whether fallback should be:
- "good enough" straight-line + average speed, or
- replaced with a custom BackupConnector (e.g., correction factor, haversine, custom terrain model).
Location ids
ExternalNodeConnectionWithLocationIdExample gives Koeln and Koeln1 the location id TestLocationId1:
// Koeln and Koeln1 share their position and therefore a location id. Every other node keeps
// its id as its location id.
INode koeln =
new TimeWindowGeoNode(
"Koeln", 50.9333, 6.95, openingHours, visitDuration, importance);
koeln.setLocationId("TestLocationId1");
opti.addElement(koeln);
INode koeln1 =
new TimeWindowGeoNode(
"Koeln1", 50.9333, 6.95, openingHours, visitDuration, importance);
koeln1.setLocationId("TestLocationId1");
opti.addElement(koeln1);
All other nodes have no explicit location id; for them, the element id is used as location id.
Connections are stored by location id. The explicit connections Koeln to Oberhausen and Oberhausen to Koeln are therefore also used for Koeln1 to Oberhausen and Oberhausen to Koeln1.
The KML file
startAndPresentResult returns the result, and example() passes it to exportToKml:
private static void exportToKml(IOptimizationResult result) throws IOException {
String name = ExternalNodeConnectionExample.class.getSimpleName();
File file = new File(name + ".kml");
EntityKMLExporter kmlExporter = new EntityKMLExporter();
kmlExporter.setTitle(name);
try (OutputStream out = new FileOutputStream(file)) {
if (!kmlExporter.export(result, out)) {
System.out.println("KML export failed: " + file);
}
}
}
The file is named after the class and written to the working directory. The stream is opened in try-with-resources, so it is closed after the export. export returns a boolean, and the example prints a line if it is false.
Events and the wait
attachToObservables subscribes to the progress, warning, status and error events before the run starts, because the subjects replay only a few past events. startAndPresentResult starts the run with startRunAsync() and waits for the result for up to 5 minutes; without the wait, the JVM would end before the run does. attachToObservables uses the event API; Recommended ways to run an optimization compares it with the legacy callback methods.
Output and what to look for
Output of ExternalNodeConnectionExample
On March 6, Jack's route holds only Aachen (20 minutes), and John's route holds the seven other nodes (705.96 minutes, 738.86 km). The routes of March 7 stay empty. The long route fits into John's 14 hours. The route items of John, trimmed from the printed result, are the same in two test runs:
2.0 Koeln1 / Geo / Arrival: 06.03.2020 08:47:59 (Europe/Berlin) , Departure: 06.03.2020 09:07:59 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 47.99 [min], 63.34 [km]
2.1 Koeln / Geo / Arrival: 06.03.2020 09:07:59 (Europe/Berlin) , Departure: 06.03.2020 09:27:59 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 0.0 [min], 0.0 [km]
2.2 Oberhausen / Geo / Arrival: 06.03.2020 09:37:59 (Europe/Berlin) , Departure: 06.03.2020 09:57:59 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 10.0 [min], 5.0 [km]
2.3 Essen / Geo / Arrival: 06.03.2020 10:06:50 (Europe/Berlin) , Departure: 06.03.2020 10:26:50 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 8.85 [min], 11.69 [km]
2.4 Wuppertal / Geo / Arrival: 06.03.2020 10:44:34 (Europe/Berlin) , Departure: 06.03.2020 11:04:34 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 17.74 [min], 23.41 [km]
2.5 Heilbronn / Geo / Arrival: 06.03.2020 14:33:15 (Europe/Berlin) , Departure: 06.03.2020 14:53:15 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 208.69 [min], 275.47 [km]
2.6 Stuttgart / Geo / Arrival: 06.03.2020 15:24:46 (Europe/Berlin) , Departure: 06.03.2020 15:44:46 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 31.52 [min], 41.6 [km]
Look for the leg into Oberhausen: 10.0 minutes and 5.0 km, the values of the connection from Koeln. In two test runs, Koeln comes right before Oberhausen. All other legs carry fallback values, for example 63.34 km and 47.99 minutes from Aachen to Koeln1. Koeln and Koeln1 have the same coordinates, so the leg between them is 0.0 km.
The run also writes ExternalNodeConnectionExample.kml, which shows the routes on a map in a KML viewer.
Output of ExternalNodeConnectionWithLocationIdExample
The route is the same as in ExternalNodeConnectionExample, except that the order of Koeln and Koeln1 can change between runs. The leg into Oberhausen shows the given values in either order, because the connection is stored for the location id that both nodes share. The leg between Koeln and Koeln1 is 0.0 minutes and 0.0 km. The line for Oberhausen is the same in two test runs:
2.2 Oberhausen / Geo / Arrival: 06.03.2020 09:37:59 (Europe/Berlin) , Departure: 06.03.2020 09:57:59 (Europe/Berlin) / Duration: 20.0 [min] / Driving: 10.0 [min], 5.0 [km]
Partial external matrix (ExternalNodeConnectionExample)
File: ExternalNodeConnectionExample.java
Intent (from toString()): "Sets the distance and the driving time between two nodes with a node connector."
Why this is a good "starter" pattern
It is the cleanest mental model:
- "Here are the edges I care about or got from an external system."
- "For all other edges, do something reasonable."
Typical real-world usage
- Provide external edges for:
- depot <-> customer,
- high-priority customers,
- cross-city edges (where straight-line is especially misleading),
- known restricted segments.
- Leave the rest to fallback to reduce integration complexity.
A recommended implementation strategy (production-grade)
Step 1: Decide your external data scope
Choose one:
- Full matrix (best accuracy; highest integration cost).
- Partial matrix + fallback (good ROI for many scenarios).
- No matrix but custom BackupConnector (low integration; can improve on the default fallback).
Step 2: Generate directed edges
Even if your external system returns symmetric values, explicitly create both directions.
Step 3: Use stable identifiers
If you use locationId, ensure it is:
- stable,
- consistent across runs,
- consistent with your geo-coordinate assignment.
Step 4: Observe and test
Read the result legs to see if the connector is applied and if a missing connection forces fallback on a leg. Attach observables (progress/status/errors) early during development so you can immediately see if infeasible time windows emerge due to travel-time inflation.
Practical "interesting" thought experiments (to validate your integration)
- Asymmetry test
Make A->B short and B->A long; verify the resulting tour order changes accordingly. - Fallback boundary test
Provide external edges for only 10% of pairs. Compare results with:- default fallback,
- a custom BackupConnector (e.g., scaled distances), to measure sensitivity.
- LocationId consolidation test
Create multiple nodes at one physical location (samelocationId) and observe whether:- the run becomes faster (smaller effective geography),
- the solution becomes more "grouped" around those clustered tasks.
Run it
Run the main method of ExternalNodeConnectionExample or ExternalNodeConnectionWithLocationIdExample from your IDE. You need a JDK in the version that the examples pom sets. You need no license file: without one, the examples use the public evaluation license (see the license guide). No other example needs to run first. ExternalNodeConnectionExample writes ExternalNodeConnectionExample.kml into the working directory.
Related
- BackupConnector feature guide: how JOpt calculates the distances and driving times of missing connections, and how to change the calculation.
- Basic elements tutorial: nodes, resources and element connections, with a connection in each direction.
- Export a result to a KML file: the KML export in an example of its own.
- Recommended ways to run an optimization: the recommended run pattern, a synchronous run and the legacy callback methods.
- Reactive events and streams: the event API that
attachToObservablesuses. - Basic examples: the overview of all Basic examples.
Added: 2026-02-17 (JOpt core 7.5.3-j17) | Last updated: 2026-10-23