The Backup-Connector

Automatically calculated distances and driving-times between Nodes and Resources are usually sufficient to solve an optimization problem. By default, a direct line between two elements is drawn to calculate the distance. Follow this direct line, the driving-time is calculated by setting an average speed. The BackupConnector can manipulate the calculations, account for external conditions, or completely exchange the algorithm used to calculate a connection.


Overview


Description - How JOptTourOptimizer calculates distances and times

By default, JOptTourOptimizer uses a spherical earth model projected on to a plane for the distance calculation (flat-surface formula).

This flat-surface formula is getting more unprecise the further away two points are from each other. However, since directly connected Nodes in an optimized solution are usually not too far away from one another (on earth-scale) the flat surface-formula is usually sufficient. Performance wise, the flat surface-formula is the more efficient of the two.

The Haversine formula is more precise as it uses the great-circle distance between two points on a sphere. When comparing distances calculated with both formulas, the difference is almost negligible.

Comparing Haversine and Flat-Surface

As an example, let's have a look at a distance of about 400 km between the cities "Cologne" and "Nuernberg":

  • Flat-surface formula: 398.76 km
  • Haversine formula: 403.73 km

The difference in the calculations of this magnitude is in the order of about 1%.

If we calculate a short distance of about 30 km between the cities "Essen" and "Wuppertal" the calculations look as follows:

  • Flat-surface formula: 28.09 km
  • Haversine formula: 28.13 km

Here, the error margin is even smaller than 0.1%.

Keep in mind, that in an optimized solution distances between consecutively visited nodes are usually small. When optimizing within cities, the distance between two successively visited Nodes is usually a few kilometres at most.

Calculation of the driving time

The calculation of the driving time is straightforward. By assuming a default average speed of avgSpeed = 22 [meter/second] the driving time can be calculated by:

drivingTime = avgSpeed x distance.

The average speed of each Resource can be modified as follows:

IResource capacityResource = new CapacityResource("John", latitude, longitude,
    maxWorkingTime, maxDistance, johnWorkingHours);

// Setting average speed to 30 meters/second
Quantity<Speed> avgSpeed = Quantities.getQuantity(30.0, Units.KILOMETRE_PER_HOUR);

capacityResource.setAvgSpeed(avgSpeed);

Please refer to the manual Basic Elements on how to create a Resource.


Modifying the distance and time calculation

Example

Find this example on GitHub: CustomNodeBackUpConnectorExample.

First of all, we need to tell the Optimizer's NodeEdgeConnector to use a custom BackupElementConnector. In our case, this will be the class MyBackupElementConnector.

// Set custom backup connector
INodeEdgeConnector connector = new NodeEdgeConnector();
connector.setBackupElementConnector(new MyBackupElementConnector(false));
optimization.setNodeConnector(connector);

Using a BackupConnector it is possible to modify the distance and driving time calculation of a connection. For this purpose, a custom class can be created that is extending DefaultFlatEarthAverageSpeedBackupElementConnector:

When constructing our class MyBackupElementConnector the constructor expects a boolean called doRecalculateElement2ElementDuration.

public class MyBackupElementConnector
      extends DefaultFlatEarthAverageSpeedBackupElementConnector {

    /**
     * Instantiates a new backup element connector.
     *
     * @param doRecalculateElement2ElementDuration the do recalculate 
     *        element 2 element duration
     */
    public MyBackupElementConnector(boolean doRecalculateElement2ElementDuration){
      super(doRecalculateElement2ElementDuration);
    }

    ...

When all defined Resources have the same average speed, every Resource will need the same time for the same connection and a recalculation will nod be necessary => doRecalculateElement2ElementDuration = false. In case Resources have different average speeds, the driving time has to be re-evaluated for each Resource separately => doRecalculateElement2ElementDuration = true.


Overriding the two methods getElement2ElementDistance(...) and getElement2ElementDuration(...) changes the default calculation of distances and driving times in JOptTourOptimizer. In the following paragraph, we implement both methods:

@Override
public Quantity<Length> getElement2ElementDistance(
    String fromElementId,
    double fromElementLon,
    double fromElementLat,
    String toElementId,
    double toElementLon,
    double toElementLat,
    IResource visitor) {

  double distance =
      NodeEdgeConnector.distancePlacePlaceFlatEarth(
              fromElementLon, fromElementLat, toElementLon, toElementLat)
          * 1.2;

  return Quantities.getQuantity(distance, METRE);
}


@Override
public Duration getElement2ElementDuration(
    String fromElementId, 
    String toElementId, 
    double distanceMeter, 
    IResource visitor) {

  long traveltimeMillis = (long) (distanceMeter / visitor.getAvgSpeed() * 1000L);

  return Duration.ofMillis(traveltimeMillis);
}

In our implementation example we use the default method NodeEdgeConnector.distancePlacePlaceFlatEarth(...) to calculate the distance between two points on the map. Further, we add a correction factor of 1.2 to each distance to factor in difficult terrain and twisty roads. From now on every connection is 1.2 times longer than what JOptTourOptimizer usually would calculate. For the driving-time we use the default implementation.


In order to apply the haversine formula, we create a new method and use it within getElement2ElementDistance(...):

public static double haversineDistanceMeter(
   double lon1,
   double lat1,
   double lon2,
       double lat2) {

    double r = 6371 * 1000.0; // Radius of the earth in meter

    double deltaLatRad = toRad(lat2 - lat1);
    double deltaLonRad = toRad(lon2 - lon1);

    double a =
        Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2)
            + Math.cos(toRad(lat1))
                * Math.cos(toRad(lat2))
                * Math.sin(deltaLonRad / 2)
                * Math.sin(deltaLonRad / 2);
    double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return r * c; // Distance in meter

}

private static Double toRad(Double value) {
    return value * Math.PI / 180;
}

Example

Find Haversine example on GitHub: CustomNodeBackUpConnectorHaversineExample.


Agreement

For reading our license agreement and for further information about license plans, please visit www.dna-evolutions.com.


Authors

A product by dna-evolutions ©