From a9bdb8d80d9bd7d60d010f76b44e590a6b2b675a Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Tue, 6 Oct 2020 17:27:50 +0200 Subject: [PATCH 01/10] Add space after argument definition --- .../jsprit/core/algorithm/state/UpdateActivityTimes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java index edf4c8dec..8f041861d 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java @@ -49,7 +49,7 @@ public class UpdateActivityTimes implements ActivityVisitor, StateUpdater { */ public UpdateActivityTimes(ForwardTransportTime transportTime, VehicleRoutingActivityCosts activityCosts) { super(); - timeTracker = new ActivityTimeTracker(transportTime,activityCosts ); + timeTracker = new ActivityTimeTracker(transportTime, activityCosts); } public UpdateActivityTimes(ForwardTransportTime transportTime, ActivityTimeTracker.ActivityPolicy activityPolicy, VehicleRoutingActivityCosts activityCosts) { From 452a8c30611bc7a0c50cfac8a2a538b6a7716de0 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Tue, 6 Oct 2020 17:28:33 +0200 Subject: [PATCH 02/10] Compute start and end times of activities with new policy The new policy will group activities together, so that their start and end times are matching if it would be possible at all. When comparing two activities to determine the service time, the greater one will be selected. --- .../jsprit/core/util/ActivityTimeTracker.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java index 4c7f20fc4..ebf0c97ef 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java @@ -27,7 +27,7 @@ public class ActivityTimeTracker implements ActivityVisitor { public static enum ActivityPolicy { - AS_SOON_AS_TIME_WINDOW_OPENS, AS_SOON_AS_ARRIVED + AS_SOON_AS_TIME_WINDOW_OPENS, AS_SOON_AS_ARRIVED, AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP } @@ -85,22 +85,39 @@ public void visit(TourActivity activity) { double transportTime = this.transportTime.getTransportTime(prevAct.getLocation(), activity.getLocation(), startAtPrevAct, route.getDriver(), route.getVehicle()); double arrivalTimeAtCurrAct = startAtPrevAct + transportTime; - actArrTime = arrivalTimeAtCurrAct; + // modify the activity arrival time if this activity can be grouped with the previous one + // they will both have the same arrival and end times afterwards + if (canGroupActivities(activity, arrivalTimeAtCurrAct)) { + actArrTime = arrivalTimeAtCurrAct - prevAct.getOperationTime(); + } else { + actArrTime = arrivalTimeAtCurrAct; + } double operationStartTime; if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS)) { operationStartTime = Math.max(activity.getTheoreticalEarliestOperationStartTime(), arrivalTimeAtCurrAct); } else if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_ARRIVED)) { operationStartTime = actArrTime; + } else if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP)) { + operationStartTime = Math.max(activity.getTheoreticalEarliestOperationStartTime(), actArrTime); } else operationStartTime = actArrTime; - double operationEndTime = operationStartTime + activityCosts.getActivityDuration(activity,actArrTime,route.getDriver(),route.getVehicle()); + double operationEndTime; + // if the current activity can be grouped with the previous one adjust the operation end time + // select the operation time which is bigger + // as we iterate over each activity, we need to change the operation end time of the previous activity so that they have the same end time + // (we didn't know when inserting the previous activity, that we should use the operating time of the current activity) + if (canGroupActivities(activity, arrivalTimeAtCurrAct)) { + operationEndTime = operationStartTime + Math.max(prevAct.getOperationTime(), activity.getOperationTime()); + prevAct.setEndTime(operationEndTime); + } else { + operationEndTime = operationStartTime + activity.getOperationTime(); + } actEndTime = operationEndTime; prevAct = activity; startAtPrevAct = operationEndTime; - } @Override @@ -115,4 +132,19 @@ public void finish() { } + private boolean canGroupActivities(TourActivity activity, double arrivalTimeAtCurrAct) { + if (!activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP)) { + return false; + } + + // group activities if the end time of the previous activity is matching the arrival time of the current activity + if (Double.compare(arrivalTimeAtCurrAct, startAtPrevAct) == 0) { + // check if the current activity could start at the same time as the previous activity by subtracting the operation time / service time + // and compare this time to the lower bound of the time window. + double theoreticalArrivalTimeAtCurrActWithoutPrevOperatingTime = arrivalTimeAtCurrAct - prevAct.getOperationTime(); + return theoreticalArrivalTimeAtCurrActWithoutPrevOperatingTime >= activity.getTheoreticalEarliestOperationStartTime(); + } + return false; + } + } From 0e45870a58c1fffa317953c12d3944676ad6c4f1 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Tue, 6 Oct 2020 17:40:07 +0200 Subject: [PATCH 03/10] Use new policy until policy is configurable --- .../com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java index a98b50fd2..72f0922ef 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java @@ -70,7 +70,7 @@ public Collection get(VehicleRoute vehicleRoute) { stateManager.addStateUpdater(twUpdater); stateManager.updateSkillStates(); - stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(), ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS, vrp.getActivityCosts())); + stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(), ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP, vrp.getActivityCosts())); stateManager.addStateUpdater(new UpdateVariableCosts(vrp.getActivityCosts(), vrp.getTransportCosts(), stateManager)); stateManager.addStateUpdater(new UpdateFutureWaitingTimes(stateManager, vrp.getTransportCosts())); } From 3f88a0f61bfca7888c4c378ed275d73867997377 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Tue, 6 Oct 2020 17:40:44 +0200 Subject: [PATCH 04/10] Add example with dynamic service times --- .../examples/DynamicServiceTimeExample.java | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java diff --git a/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java b/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java new file mode 100644 index 000000000..2ec7d4895 --- /dev/null +++ b/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java @@ -0,0 +1,156 @@ +package com.graphhopper.jsprit.examples; + +import com.graphhopper.jsprit.core.algorithm.VehicleRoutingAlgorithm; +import com.graphhopper.jsprit.core.algorithm.box.Jsprit; +import com.graphhopper.jsprit.core.algorithm.state.StateManager; +import com.graphhopper.jsprit.core.algorithm.state.UpdateActivityTimes; +import com.graphhopper.jsprit.core.problem.Location; +import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; +import com.graphhopper.jsprit.core.problem.constraint.ConstraintManager; +import com.graphhopper.jsprit.core.problem.cost.VehicleRoutingTransportCosts; +import com.graphhopper.jsprit.core.problem.job.Shipment; +import com.graphhopper.jsprit.core.problem.solution.SolutionCostCalculator; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; +import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleImpl; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleType; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleTypeImpl; +import com.graphhopper.jsprit.core.reporting.SolutionPrinter; +import com.graphhopper.jsprit.core.util.ActivityTimeTracker; +import com.graphhopper.jsprit.core.util.Coordinate; +import com.graphhopper.jsprit.core.util.Solutions; +import com.graphhopper.jsprit.core.util.VehicleRoutingTransportCostsMatrix; +import com.graphhopper.jsprit.util.Examples; + +import java.time.Instant; +import java.util.Collection; + +public class DynamicServiceTimeExample { + + public static void main(String[] args) { + /* + * some preparation - create output folder + */ + Examples.createOutputFolder(); + + //define a symmetric travel time matrix + VehicleRoutingTransportCostsMatrix.Builder costMatrixBuilder = VehicleRoutingTransportCostsMatrix.Builder.newInstance(true); + costMatrixBuilder.addTransportTime("vehicle:location", "shipment:pickup", 60D * 1); + costMatrixBuilder.addTransportTime("vehicle:location", "shipment:dropoff", 60D * 2); + costMatrixBuilder.addTransportTime("shipment:pickup", "shipment:dropoff", 60D * 1); + costMatrixBuilder.addTransportTime("vehicle:location", "vehicle:location", 0D); + costMatrixBuilder.addTransportTime("shipment:pickup", "shipment:pickup", 0D); + costMatrixBuilder.addTransportTime("shipment:dropoff", "shipment:dropoff", 0D); + + costMatrixBuilder.addTransportTime("vehicle:location", "new:pickup", 60D * 1); + costMatrixBuilder.addTransportTime("vehicle:location", "new:dropoff", 60D * 2); + costMatrixBuilder.addTransportTime("new:pickup", "new:dropoff", 60D * 1); + costMatrixBuilder.addTransportTime("new:pickup", "new:pickup", 0D); + costMatrixBuilder.addTransportTime("new:dropoff", "new:dropoff", 0D); + + costMatrixBuilder.addTransportTime("new:pickup", "shipment:dropoff", 60D * 1); + costMatrixBuilder.addTransportTime("new:dropoff", "shipment:pickup", 60D * 1); + costMatrixBuilder.addTransportTime("new:pickup", "shipment:pickup", 0D); + costMatrixBuilder.addTransportTime("new:dropoff", "shipment:dropoff", 0D); + + VehicleRoutingTransportCosts costMatrix = costMatrixBuilder.build(); + + Instant vehicleStartTime = Instant.parse("2020-10-05T12:00:00Z"); + Instant vehicleEndTime = Instant.parse("2020-10-05T14:00:00Z"); + VehicleType type = VehicleTypeImpl.Builder.newInstance("type") + .addCapacityDimension(0, 7) + .setCostPerTransportTime(1) + .setCostPerDistance(0.0) + .setCostPerWaitingTime(0.0) + .setCostPerServiceTime(0.0) + .build(); + VehicleImpl vehicle = VehicleImpl.Builder.newInstance("vehicle") + .setStartLocation(Location.Builder.newInstance().setId("vehicle:location").setCoordinate(Coordinate.newInstance(0, 0)).build()) + .setEarliestStart(vehicleStartTime.getEpochSecond()) + .setLatestArrival(vehicleEndTime.getEpochSecond()) + .setReturnToDepot(false) + .setType(type) + .build(); + + Instant earliestPickupTime = Instant.parse("2020-10-05T12:00:00Z"); + Instant latestPickupTime = Instant.parse("2020-10-05T12:05:00Z"); + Instant earliestDeliveryTime = Instant.parse("2020-10-05T12:01:00Z"); + Instant latestDeliveryTime = Instant.parse("2020-10-05T12:10:00Z"); + Shipment shipment = Shipment.Builder.newInstance("shipment") + .addSizeDimension(0, 1) + .setPickupLocation(Location.Builder.newInstance().setId("shipment:pickup").setCoordinate(Coordinate.newInstance(2, 2)).build()) + .setDeliveryLocation(Location.Builder.newInstance().setId("shipment:dropoff").setCoordinate(Coordinate.newInstance(4, 4)).build()) + .addPickupTimeWindow(earliestPickupTime.getEpochSecond(), latestPickupTime.getEpochSecond()) + .addDeliveryTimeWindow(earliestDeliveryTime.getEpochSecond(), latestDeliveryTime.getEpochSecond()) + .setDeliveryServiceTime(120D) + .setPickupServiceTime(120D) + .build(); + + Instant newEarliestPickupTime = Instant.parse("2020-10-05T12:00:00Z"); + Instant newLatestPickupTime = Instant.parse("2020-10-05T12:05:00Z"); + Instant newEarliestDeliveryTime = Instant.parse("2020-10-05T12:01:00Z"); + Instant newLatestDeliveryTime = Instant.parse("2020-10-05T12:10:00Z"); + Shipment newShipment = Shipment.Builder.newInstance("new") + .addSizeDimension(0, 1) + .setPickupLocation(Location.Builder.newInstance().setId("new:pickup").setCoordinate(Coordinate.newInstance(2, 2)).build()) + .setDeliveryLocation(Location.Builder.newInstance().setId("new:dropoff").setCoordinate(Coordinate.newInstance(4, 4)).build()) + .addPickupTimeWindow(newEarliestPickupTime.getEpochSecond(), newLatestPickupTime.getEpochSecond()) + .addDeliveryTimeWindow(newEarliestDeliveryTime.getEpochSecond(), newLatestDeliveryTime.getEpochSecond()) + .setDeliveryServiceTime(60D) + .setPickupServiceTime(60D) + .build(); + + VehicleRoute vehicleRoute = VehicleRoute.Builder.newInstance(vehicle) + .addPickup(shipment) + .addDelivery(shipment) + .build(); + + VehicleRoutingProblem vrp = VehicleRoutingProblem.Builder.newInstance() + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .setRoutingCost(costMatrix) + .addInitialVehicleRoute(vehicleRoute) + .addJob(newShipment) + .build(); + + SolutionCostCalculator objectiveFunction = new SolutionCostCalculator() { + @Override + public double getCosts(VehicleRoutingProblemSolution solution) { + double costs = 0; + for (VehicleRoute route : solution.getRoutes()) { + for (TourActivity activity : route.getActivities()) { + costs += vrp.getActivityCosts().getActivityCost(activity, activity.getArrTime(), route.getDriver(), route.getVehicle()); + } + } + return costs; + } + }; + + StateManager stateManager = new StateManager(vrp); + stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(),ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP, vrp.getActivityCosts())); + + ConstraintManager constraintManager = new ConstraintManager(vrp, stateManager); + constraintManager.addTimeWindowConstraint(); + constraintManager.addLoadConstraint(); + constraintManager.addSkillsConstraint(); + + VehicleRoutingAlgorithm vra = Jsprit.Builder.newInstance(vrp) + .setStateAndConstraintManager(stateManager, constraintManager) + .setObjectiveFunction(objectiveFunction) + .setProperty(Jsprit.Parameter.FAST_REGRET, "true") + .buildAlgorithm(); + + Collection solutions = vra.searchSolutions(); + + System.out.println("---------------------- Route Activity Times ----------------------"); + Solutions.bestOf(solutions).getRoutes().stream().forEach(route -> { + route.getActivities().stream().forEach(activity -> { + System.out.println("Arrival time " + activity.getName() + " " + Instant.ofEpochSecond((long) activity.getArrTime())); + System.out.println("Departure time " + activity.getName() + " " + Instant.ofEpochSecond((long) activity.getEndTime())); + }); + }); + System.out.println("---------------------- Route Activity Times ----------------------"); + + SolutionPrinter.print(vrp, Solutions.bestOf(solutions), SolutionPrinter.Print.VERBOSE); + } +} From 7c73573fe4635d08d079027ee2d05ee81c87a06b Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Fri, 23 Oct 2020 13:29:44 +0200 Subject: [PATCH 05/10] Revert "Use new policy until policy is configurable" This reverts commit 0e45870a58c1fffa317953c12d3944676ad6c4f1. --- .../com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java index 72f0922ef..a98b50fd2 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/AlgorithmUtil.java @@ -70,7 +70,7 @@ public Collection get(VehicleRoute vehicleRoute) { stateManager.addStateUpdater(twUpdater); stateManager.updateSkillStates(); - stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(), ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP, vrp.getActivityCosts())); + stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(), ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS, vrp.getActivityCosts())); stateManager.addStateUpdater(new UpdateVariableCosts(vrp.getActivityCosts(), vrp.getTransportCosts(), stateManager)); stateManager.addStateUpdater(new UpdateFutureWaitingTimes(stateManager, vrp.getTransportCosts())); } From 27b27daa8c7046a0207b8fce6b397fbea1f8a148 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Fri, 23 Oct 2020 13:30:04 +0200 Subject: [PATCH 06/10] Revert "Compute start and end times of activities with new policy" This reverts commit 452a8c30611bc7a0c50cfac8a2a538b6a7716de0. --- .../jsprit/core/util/ActivityTimeTracker.java | 40 ++----------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java index ebf0c97ef..4c7f20fc4 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/util/ActivityTimeTracker.java @@ -27,7 +27,7 @@ public class ActivityTimeTracker implements ActivityVisitor { public static enum ActivityPolicy { - AS_SOON_AS_TIME_WINDOW_OPENS, AS_SOON_AS_ARRIVED, AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP + AS_SOON_AS_TIME_WINDOW_OPENS, AS_SOON_AS_ARRIVED } @@ -85,39 +85,22 @@ public void visit(TourActivity activity) { double transportTime = this.transportTime.getTransportTime(prevAct.getLocation(), activity.getLocation(), startAtPrevAct, route.getDriver(), route.getVehicle()); double arrivalTimeAtCurrAct = startAtPrevAct + transportTime; - // modify the activity arrival time if this activity can be grouped with the previous one - // they will both have the same arrival and end times afterwards - if (canGroupActivities(activity, arrivalTimeAtCurrAct)) { - actArrTime = arrivalTimeAtCurrAct - prevAct.getOperationTime(); - } else { - actArrTime = arrivalTimeAtCurrAct; - } + actArrTime = arrivalTimeAtCurrAct; double operationStartTime; if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS)) { operationStartTime = Math.max(activity.getTheoreticalEarliestOperationStartTime(), arrivalTimeAtCurrAct); } else if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_ARRIVED)) { operationStartTime = actArrTime; - } else if (activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP)) { - operationStartTime = Math.max(activity.getTheoreticalEarliestOperationStartTime(), actArrTime); } else operationStartTime = actArrTime; - double operationEndTime; - // if the current activity can be grouped with the previous one adjust the operation end time - // select the operation time which is bigger - // as we iterate over each activity, we need to change the operation end time of the previous activity so that they have the same end time - // (we didn't know when inserting the previous activity, that we should use the operating time of the current activity) - if (canGroupActivities(activity, arrivalTimeAtCurrAct)) { - operationEndTime = operationStartTime + Math.max(prevAct.getOperationTime(), activity.getOperationTime()); - prevAct.setEndTime(operationEndTime); - } else { - operationEndTime = operationStartTime + activity.getOperationTime(); - } + double operationEndTime = operationStartTime + activityCosts.getActivityDuration(activity,actArrTime,route.getDriver(),route.getVehicle()); actEndTime = operationEndTime; prevAct = activity; startAtPrevAct = operationEndTime; + } @Override @@ -132,19 +115,4 @@ public void finish() { } - private boolean canGroupActivities(TourActivity activity, double arrivalTimeAtCurrAct) { - if (!activityPolicy.equals(ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP)) { - return false; - } - - // group activities if the end time of the previous activity is matching the arrival time of the current activity - if (Double.compare(arrivalTimeAtCurrAct, startAtPrevAct) == 0) { - // check if the current activity could start at the same time as the previous activity by subtracting the operation time / service time - // and compare this time to the lower bound of the time window. - double theoreticalArrivalTimeAtCurrActWithoutPrevOperatingTime = arrivalTimeAtCurrAct - prevAct.getOperationTime(); - return theoreticalArrivalTimeAtCurrActWithoutPrevOperatingTime >= activity.getTheoreticalEarliestOperationStartTime(); - } - return false; - } - } From 9330240cbf37d386a22eb01cb30f71af1f8ecbef Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Mon, 26 Oct 2020 16:46:35 +0100 Subject: [PATCH 07/10] Group activities together if possible by adjusting their times After all activities have been inserted in a route and the ActivityTimeTracker has updated the times of the activites, check if some of them could be group together. A activity belongs to a group of activities if their locations are the same and if they could be served at the same time. This is achieved by iterating over the activites for a route when `UpdateActivityTimes.finish` is called. --- .../algorithm/state/UpdateActivityTimes.java | 68 ++++++++++++++++++- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java index 8f041861d..77c049e93 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java @@ -17,6 +17,7 @@ */ package com.graphhopper.jsprit.core.algorithm.state; +import com.graphhopper.jsprit.core.problem.Location; import com.graphhopper.jsprit.core.problem.cost.ForwardTransportTime; import com.graphhopper.jsprit.core.problem.cost.VehicleRoutingActivityCosts; import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; @@ -24,6 +25,9 @@ import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity; import com.graphhopper.jsprit.core.util.ActivityTimeTracker; +import java.util.ArrayList; +import java.util.List; + /** * Updates arrival and end times of activities. @@ -34,7 +38,7 @@ */ public class UpdateActivityTimes implements ActivityVisitor, StateUpdater { - private ActivityTimeTracker timeTracker; + private final ActivityTimeTracker timeTracker; private VehicleRoute route; @@ -48,7 +52,6 @@ public class UpdateActivityTimes implements ActivityVisitor, StateUpdater { * activity.getEndTime() */ public UpdateActivityTimes(ForwardTransportTime transportTime, VehicleRoutingActivityCosts activityCosts) { - super(); timeTracker = new ActivityTimeTracker(transportTime, activityCosts); } @@ -73,7 +76,66 @@ public void visit(TourActivity activity) { @Override public void finish() { timeTracker.finish(); - route.getEnd().setArrTime(timeTracker.getActArrTime()); + List activities = route.getActivities(); + double totalSavedTime = 0; + for (int i = 0; i < activities.size(); i++) { + TourActivity current = activities.get(i); + double endTime = current.getEndTime(); + double accumulatedOperatingTime = 0; + double savedTimeByGrouping = 0; + double maximumOperatingTime = current.getOperationTime(); + double minimumOperatingTime = current.getOperationTime(); + List groupedActivities = new ArrayList<>(); + for (int j = i + 1; j < activities.size(); j++) { + TourActivity next = activities.get(j); + if (isSameLocation(current.getLocation(), next.getLocation()) && shouldOperateAtSameTime(next, endTime, accumulatedOperatingTime + next.getOperationTime())) { + accumulatedOperatingTime += next.getOperationTime(); + maximumOperatingTime = Math.max(maximumOperatingTime, next.getOperationTime()); + minimumOperatingTime = Math.min(minimumOperatingTime, next.getOperationTime()); + savedTimeByGrouping += minimumOperatingTime; + groupedActivities.add(next); + i++; + } else { + break; + } + } + // if activities have been grouped before, adjust the arrival time for the vehicle by the previously accumulated saved time + current.setArrTime(current.getArrTime() - totalSavedTime); + current.setEndTime(endTime - current.getOperationTime() + maximumOperatingTime - totalSavedTime); + groupedActivities.forEach(activity -> { + activity.setArrTime(current.getArrTime()); + activity.setEndTime(current.getEndTime()); + }); + // Adjust the saved time by comparing the savedTimePerGroup with the difference to the accumulated time. + // Three activities with the service time of 1 each + // - accumulatedOperatingTime = 2 + // - savedTimeByGrouping = 2 + // - total saved time = max(0, 2) = 2 + // Three activities with the service time of 1, 2, 1 respectively: + // - accumulatedOperatingTime = 3 + // - savedTimeByGrouping = 2 + // - total saved time = max(1, 2) = 2 + // Three activities with the service time of 1, 3, 2 respectively: + // - accumulatedOperatingTime = 5 + // - savedTimeByGrouping = 2 + // - total saved time = max(3, 2) = 3 + totalSavedTime += Math.max(accumulatedOperatingTime - savedTimeByGrouping, savedTimeByGrouping); + } + + route.getEnd().setArrTime(timeTracker.getActArrTime() - totalSavedTime); + } + + private boolean isSameLocation(Location location, Location other) { + double maxDelta = 0.000001; + double diffLng = Math.abs(location.getCoordinate().getX() - other.getCoordinate().getX()); + double diffLat = Math.abs(location.getCoordinate().getY() - other.getCoordinate().getY()); + return diffLat < maxDelta && diffLng < maxDelta; + } + + private boolean shouldOperateAtSameTime(TourActivity next, double endTime, double accumulatedOperatingTime) { + boolean similarOperatingTime = Math.abs(next.getEndTime() - accumulatedOperatingTime - endTime) <= 0.001; + boolean laterThanTheoreticalStart = next.getEndTime() - accumulatedOperatingTime >= next.getTheoreticalEarliestOperationStartTime(); + return similarOperatingTime && laterThanTheoreticalStart; } } From 609c5a5f0fa7a092df74d3fac9878d9745001725 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Mon, 26 Oct 2020 16:50:06 +0100 Subject: [PATCH 08/10] Adapt example after last changes --- .../jsprit/examples/DynamicServiceTimeExample.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java b/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java index 2ec7d4895..849db5118 100644 --- a/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java +++ b/jsprit-examples/src/main/java/com/graphhopper/jsprit/examples/DynamicServiceTimeExample.java @@ -2,11 +2,8 @@ import com.graphhopper.jsprit.core.algorithm.VehicleRoutingAlgorithm; import com.graphhopper.jsprit.core.algorithm.box.Jsprit; -import com.graphhopper.jsprit.core.algorithm.state.StateManager; -import com.graphhopper.jsprit.core.algorithm.state.UpdateActivityTimes; import com.graphhopper.jsprit.core.problem.Location; import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; -import com.graphhopper.jsprit.core.problem.constraint.ConstraintManager; import com.graphhopper.jsprit.core.problem.cost.VehicleRoutingTransportCosts; import com.graphhopper.jsprit.core.problem.job.Shipment; import com.graphhopper.jsprit.core.problem.solution.SolutionCostCalculator; @@ -17,7 +14,6 @@ import com.graphhopper.jsprit.core.problem.vehicle.VehicleType; import com.graphhopper.jsprit.core.problem.vehicle.VehicleTypeImpl; import com.graphhopper.jsprit.core.reporting.SolutionPrinter; -import com.graphhopper.jsprit.core.util.ActivityTimeTracker; import com.graphhopper.jsprit.core.util.Coordinate; import com.graphhopper.jsprit.core.util.Solutions; import com.graphhopper.jsprit.core.util.VehicleRoutingTransportCostsMatrix; @@ -126,16 +122,8 @@ public double getCosts(VehicleRoutingProblemSolution solution) { } }; - StateManager stateManager = new StateManager(vrp); - stateManager.addStateUpdater(new UpdateActivityTimes(vrp.getTransportCosts(),ActivityTimeTracker.ActivityPolicy.AS_SOON_AS_TIME_WINDOW_OPENS_WITHIN_GROUP, vrp.getActivityCosts())); - - ConstraintManager constraintManager = new ConstraintManager(vrp, stateManager); - constraintManager.addTimeWindowConstraint(); - constraintManager.addLoadConstraint(); - constraintManager.addSkillsConstraint(); - VehicleRoutingAlgorithm vra = Jsprit.Builder.newInstance(vrp) - .setStateAndConstraintManager(stateManager, constraintManager) + .addCoreStateAndConstraintStuff(true) .setObjectiveFunction(objectiveFunction) .setProperty(Jsprit.Parameter.FAST_REGRET, "true") .buildAlgorithm(); From fbc1df1bef8701ba9c0e2d6d736eb48f2b393e93 Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Mon, 26 Oct 2020 17:22:33 +0100 Subject: [PATCH 09/10] Fall back to default location comparison When coordinates are not provided for a location, fall back to the object comparison for locations `loc.equals(other)` to evaluate if two activities happen at the same location. --- .../core/algorithm/state/UpdateActivityTimes.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java index 77c049e93..bfd01dfc1 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimes.java @@ -126,10 +126,13 @@ public void finish() { } private boolean isSameLocation(Location location, Location other) { - double maxDelta = 0.000001; - double diffLng = Math.abs(location.getCoordinate().getX() - other.getCoordinate().getX()); - double diffLat = Math.abs(location.getCoordinate().getY() - other.getCoordinate().getY()); - return diffLat < maxDelta && diffLng < maxDelta; + if (location.getCoordinate() != null && other.getCoordinate() != null) { + double maxDelta = 0.000001; + double diffLng = Math.abs(location.getCoordinate().getX() - other.getCoordinate().getX()); + double diffLat = Math.abs(location.getCoordinate().getY() - other.getCoordinate().getY()); + return diffLat < maxDelta && diffLng < maxDelta; + } + return location.equals(other); } private boolean shouldOperateAtSameTime(TourActivity next, double endTime, double accumulatedOperatingTime) { From 9c5da69a26eb708954147bdf055c6dc23af6d00f Mon Sep 17 00:00:00 2001 From: Marcel Radischat Date: Mon, 26 Oct 2020 17:31:08 +0100 Subject: [PATCH 10/10] Add unit tests for UpdateActivityTimes --- .../state/UpdateActivityTimesTest.java | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimesTest.java diff --git a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimesTest.java b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimesTest.java new file mode 100644 index 000000000..b465ffb01 --- /dev/null +++ b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/state/UpdateActivityTimesTest.java @@ -0,0 +1,313 @@ +package com.graphhopper.jsprit.core.algorithm.state; + +import com.graphhopper.jsprit.core.problem.Location; +import com.graphhopper.jsprit.core.problem.cost.ForwardTransportTime; +import com.graphhopper.jsprit.core.problem.cost.VehicleRoutingActivityCosts; +import com.graphhopper.jsprit.core.problem.cost.WaitingTimeCosts; +import com.graphhopper.jsprit.core.problem.job.Service; +import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; +import com.graphhopper.jsprit.core.problem.solution.route.activity.*; +import com.graphhopper.jsprit.core.util.Coordinate; +import com.graphhopper.jsprit.core.util.CrowFlyCosts; +import com.graphhopper.jsprit.core.util.Locations; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UpdateActivityTimesTest{ + ForwardTransportTime transportTime; + VehicleRoutingActivityCosts activityCosts; + VehicleRoute route; + List tourActivities; + Location startLocation; + Location endLocation; + Start start; + End end; + Map coordinates; + Locations locations; + TourActivityFactory tourActivityFactory; + UpdateActivityTimes stateUpdater; + + @Before + public void setUp() throws Exception { + coordinates = new HashMap<>(); + locations = new Locations() { + @Override + public Coordinate getCoord(String id) { + return coordinates.get(id); + } + }; + Coordinate startCoordinate = Coordinate.newInstance(0, 0); + Coordinate endCoordinate = Coordinate.newInstance(10, 0); + coordinates.put("start", startCoordinate); + coordinates.put("end", endCoordinate); + startLocation = Location.Builder.newInstance().setId("start").setCoordinate(startCoordinate).build(); + endLocation = Location.Builder.newInstance().setId("end").setCoordinate(endCoordinate).build(); + start = Start.newInstance("start", 0, 0); + end = End.newInstance("end", 0, 30); + start.setLocation(startLocation); + end.setLocation(endLocation); + transportTime = new CrowFlyCosts(locations); + activityCosts = new WaitingTimeCosts(); + stateUpdater = new UpdateActivityTimes(transportTime, activityCosts); + tourActivityFactory = new DefaultTourActivityFactory(); + tourActivities = new ArrayList<>(); + route = mock(VehicleRoute.class); + when(route.getStart()).thenReturn(start); + when(route.getEnd()).thenReturn(end); + when(route.getActivities()).thenReturn(tourActivities); + } + + @Test + public void shouldNotAdjustActivityTimes_WhenActivitiesHappenAtDifferentLocations() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(4, 0); + Coordinate coordinateThree = Coordinate.newInstance(6, 0); + Coordinate coordinateFour = Coordinate.newInstance(8, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(1D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(1D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(1D).build()); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 3D, 0.001); + assertEquals(activityTwo.getArrTime(), 5D, 0.001); + assertEquals(activityTwo.getEndTime(), 6D, 0.001); + assertEquals(activityThree.getArrTime(), 8D, 0.001); + assertEquals(activityThree.getEndTime(), 9D, 0.001); + assertEquals(activityFour.getArrTime(), 11D, 0.001); + assertEquals(activityFour.getEndTime(), 12D, 0.001); + assertEquals(end.getArrTime(), 14D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } + + @Test + public void shouldNotAdjustActivityTimes_WhenActivitiesHappenAtDifferentTimes() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(2, 0); + Coordinate coordinateThree = Coordinate.newInstance(2, 0); + Coordinate coordinateFour = Coordinate.newInstance(2, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(1D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(1D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(1D).build()); + activityOne.setTheoreticalEarliestOperationStartTime(0); + activityTwo.setTheoreticalEarliestOperationStartTime(6); + activityThree.setTheoreticalEarliestOperationStartTime(10); + activityFour.setTheoreticalEarliestOperationStartTime(15); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 3D, 0.001); + assertEquals(activityTwo.getArrTime(), 3D, 0.001); + assertEquals(activityTwo.getEndTime(), 7D, 0.001); + assertEquals(activityThree.getArrTime(), 7D, 0.001); + assertEquals(activityThree.getEndTime(), 11D, 0.001); + assertEquals(activityFour.getArrTime(), 11D, 0.001); + assertEquals(activityFour.getEndTime(), 16D, 0.001); + assertEquals(end.getArrTime(), 24D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } + + @Test + public void shouldAdjustActivityTimes_WhenAllActivitiesHappenAtSameLocationAndTime() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(2, 0); + Coordinate coordinateThree = Coordinate.newInstance(2, 0); + Coordinate coordinateFour = Coordinate.newInstance(2, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(1D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(1D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(1D).build()); + activityOne.setTheoreticalEarliestOperationStartTime(0); + activityTwo.setTheoreticalEarliestOperationStartTime(0); + activityThree.setTheoreticalEarliestOperationStartTime(0); + activityFour.setTheoreticalEarliestOperationStartTime(0); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 3D, 0.001); + assertEquals(activityTwo.getArrTime(), 2D, 0.001); + assertEquals(activityTwo.getEndTime(), 3D, 0.001); + assertEquals(activityThree.getArrTime(), 2D, 0.001); + assertEquals(activityThree.getEndTime(), 3D, 0.001); + assertEquals(activityFour.getArrTime(), 2D, 0.001); + assertEquals(activityFour.getEndTime(), 3D, 0.001); + assertEquals(end.getArrTime(), 11D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } + + @Test + public void shouldAdjustActivityTimes_WhenSomeActivitiesHappenAtSameLocationAndTime() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(2, 0); + Coordinate coordinateThree = Coordinate.newInstance(4, 0); + Coordinate coordinateFour = Coordinate.newInstance(4, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(1D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(1D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(1D).build()); + activityOne.setTheoreticalEarliestOperationStartTime(0); + activityTwo.setTheoreticalEarliestOperationStartTime(0); + activityThree.setTheoreticalEarliestOperationStartTime(0); + activityFour.setTheoreticalEarliestOperationStartTime(0); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 3D, 0.001); + assertEquals(activityTwo.getArrTime(), 2D, 0.001); + assertEquals(activityTwo.getEndTime(), 3D, 0.001); + assertEquals(activityThree.getArrTime(), 5D, 0.001); + assertEquals(activityThree.getEndTime(), 6D, 0.001); + assertEquals(activityFour.getArrTime(), 5D, 0.001); + assertEquals(activityFour.getEndTime(), 6D, 0.001); + assertEquals(end.getArrTime(), 12D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } + + @Test + public void shouldAdjustActivityTimes_WhenAllActivitiesHappenAtSameLocationAndTimeAndDifferentServiceTimes() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(2, 0); + Coordinate coordinateThree = Coordinate.newInstance(2, 0); + Coordinate coordinateFour = Coordinate.newInstance(2, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(4D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(2D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(3D).build()); + activityOne.setTheoreticalEarliestOperationStartTime(0); + activityTwo.setTheoreticalEarliestOperationStartTime(0); + activityThree.setTheoreticalEarliestOperationStartTime(0); + activityFour.setTheoreticalEarliestOperationStartTime(0); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 6D, 0.001); + assertEquals(activityTwo.getArrTime(), 2D, 0.001); + assertEquals(activityTwo.getEndTime(), 6D, 0.001); + assertEquals(activityThree.getArrTime(), 2D, 0.001); + assertEquals(activityThree.getEndTime(), 6D, 0.001); + assertEquals(activityFour.getArrTime(), 2D, 0.001); + assertEquals(activityFour.getEndTime(), 6D, 0.001); + assertEquals(end.getArrTime(), 14D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } + + @Test + public void shouldAdjustActivityTimes_WhenAllActivitiesHappenAtSameLocationAndTimeAndOneDifferentServiceTime() { + Coordinate coordinateOne = Coordinate.newInstance(2, 0); + Coordinate coordinateTwo = Coordinate.newInstance(2, 0); + Coordinate coordinateThree = Coordinate.newInstance(2, 0); + Coordinate coordinateFour = Coordinate.newInstance(2, 0); + Location locationOne = Location.Builder.newInstance().setId("one").setCoordinate(coordinateOne).build(); + Location locationTwo = Location.Builder.newInstance().setId("two").setCoordinate(coordinateTwo).build(); + Location locationThree = Location.Builder.newInstance().setId("three").setCoordinate(coordinateThree).build(); + Location locationFour = Location.Builder.newInstance().setId("four").setCoordinate(coordinateFour).build(); + coordinates.put("one", coordinateOne); + coordinates.put("two", coordinateTwo); + coordinates.put("three", coordinateThree); + coordinates.put("four", coordinateFour); + TourActivity activityOne = tourActivityFactory.createActivity(Service.Builder.newInstance("one").setLocation(locationOne).setServiceTime(1D).build()); + TourActivity activityTwo = tourActivityFactory.createActivity(Service.Builder.newInstance("two").setLocation(locationTwo).setServiceTime(4D).build()); + TourActivity activityThree = tourActivityFactory.createActivity(Service.Builder.newInstance("three").setLocation(locationThree).setServiceTime(1D).build()); + TourActivity activityFour = tourActivityFactory.createActivity(Service.Builder.newInstance("four").setLocation(locationFour).setServiceTime(1D).build()); + activityOne.setTheoreticalEarliestOperationStartTime(0); + activityTwo.setTheoreticalEarliestOperationStartTime(0); + activityThree.setTheoreticalEarliestOperationStartTime(0); + activityFour.setTheoreticalEarliestOperationStartTime(0); + tourActivities.addAll(Arrays.asList(activityOne, activityTwo, activityThree, activityFour)); + stateUpdater.begin(route); + stateUpdater.visit(activityOne); + stateUpdater.visit(activityTwo); + stateUpdater.visit(activityThree); + stateUpdater.visit(activityFour); + stateUpdater.finish(); + + assertEquals(activityOne.getArrTime(), 2D, 0.001); + assertEquals(activityOne.getEndTime(), 6D, 0.001); + assertEquals(activityTwo.getArrTime(), 2D, 0.001); + assertEquals(activityTwo.getEndTime(), 6D, 0.001); + assertEquals(activityThree.getArrTime(), 2D, 0.001); + assertEquals(activityThree.getEndTime(), 6D, 0.001); + assertEquals(activityFour.getArrTime(), 2D, 0.001); + assertEquals(activityFour.getEndTime(), 6D, 0.001); + assertEquals(end.getArrTime(), 14D, 0.001); + assertEquals(end.getEndTime(), 30D, 0.001); + } +}