Step 6: Adding Trucks and TransportationTasks

The Truck class

Create a new Truck class in the 'tutorial.model' package:

Truck.java
package tutorial.model;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import com.amalgamasimulation.engine.Engine;

public class Truck {
    private static final double OWNERSHIP_COST_PER_HOUR_USD = 10;
    private static final double USAGE_COST_PER_HOUR_USD = 25;
    
    private final String id;
    private final String name;
    private final double speed;
    private final Engine engine;
    
    private record ActivePeriod(double startTime, double endTime) {}

    private List<ActivePeriod> activePeriods = new ArrayList<>();
    private Optional<Double> currentActivePeriodStartTime = Optional.empty();
    private Asset currentAsset;
    private TransportationTask currentTask;
    private List<TransportationTask> taskHistory = new ArrayList<>();

    public Truck(String id, String name, double speed, Store initialStore, Engine engine) {
        this.id = id;
        this.name = name;
        this.engine = engine;
        this.speed = speed;
        this.currentAsset = initialStore;
    }

    public String getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }
    
    public double getSpeed() {
        return speed;
    }
    
    public Asset getCurrentAsset() {
        return currentAsset;
    }
    
    public TransportationTask getCurrentTask() {
        return currentTask;
    }
    
    public List<TransportationTask> getTaskHistory() {
        return taskHistory;
    }
    
    public boolean isIdle() {
        return currentTask == null;
    }

    public double getExpenses() {
        double ownershipDurationHours = engine.time() / engine.hour();
        double usageDurationHours = getAllActivePeriodsDurationHrs();
        return ownershipDurationHours * OWNERSHIP_COST_PER_HOUR_USD + usageDurationHours * USAGE_COST_PER_HOUR_USD; 
    }

    private double getAllActivePeriodsDurationHrs() {
        double result = activePeriods.stream().mapToDouble(p -> p.endTime - p.startTime).sum();
        if (currentActivePeriodStartTime.isPresent()) {
            result += (engine.time() - currentActivePeriodStartTime.get()) / engine.hour();
        }
        return result;
    }

    public void onTaskStarted(TransportationTask task) {
        currentActivePeriodStartTime = Optional.of(engine.time());
        currentTask = task;
        taskHistory.add(currentTask);
    }

    public void onTaskCompleted() {
        activePeriods.add(new ActivePeriod(currentActivePeriodStartTime.get(), engine.time()));
        currentActivePeriodStartTime = Optional.empty();
        currentAsset = currentTask.getRequest().getDestAsset();
        currentTask = null;
    }
}

A Truck reports whether it is available for a new transportation using its isIdle() method.

A Truck becomes busy when its onTaskStarted(task) method is called. The truck is free again after calling its onTaskCompleted() method. Both methods will be called from outside a truck.

Ownership duration is simply the current model time (in hours) returned by the Engine.time() / Engine.hour() methods call. The Engine.hour() method returns the number of units of model time in one model hour. So, for example, if we later decide to switch to another time unit and measure model time in minutes instead of hours (by calling Engine’s setTemporal() method), we can be sure that Engine.time() / Engine.hour() always returns the number of hours, not minutes, and our expenses calculations are still correct.

Truck usage duration is calculated by the transportation time intervals.

Here we use the Utils.zidz() method, it helps to divide numbers with no risk of division-by-zero exception.

The TransportationTask class

The TransportationTask class will contain the answer to the question: "What does a truck need to do when assigned a request?"

Create a new TransportationTask class in the 'tutorial.model' package:

TransportationTask.java
package tutorial.model;

import java.util.function.Consumer;

public class TransportationTask {
    
    public enum Status {
        NOT_STARTED, IN_PROGRESS, COMPLETED_ON_TIME, COMPLETED_AFTER_DEADLINE;
    }
    
    private final String id;
    private Truck truck;
    private final TransportationRequest request;
    private final Consumer<TransportationTask> taskCompletedHandler;
    private final Model model;
    private double beginTime;

    public TransportationTask(String id, TransportationRequest request,
            Consumer<TransportationTask> taskCompletedHandler, Model model) {
        this.id = id;
        this.request = request;
        this.taskCompletedHandler = taskCompletedHandler;
        this.model = model;
    }

    public String getId() {
        return id;
    }

    public Truck getTruck() {
        return truck;
    }

    public Status getStatus() {
        if (truck == null) {
            return Status.NOT_STARTED;
        }
        if (request.isCompleted()) {
            return request.getCompletedTime() <= request.getDeadlineTime() ? Status.COMPLETED_ON_TIME : Status.COMPLETED_AFTER_DEADLINE;
        }
        return Status.IN_PROGRESS;
    }
    
    public TransportationRequest getRequest() {
        return request;
    }
    
    public double getBeginTime() {
        return beginTime;
    }

    public void execute(Truck truck) {
        this.truck = truck;
        this.beginTime = model.engine().time();
        double toWarehouseDistance = model.getRouteLength(truck.getCurrentAsset(), request.getSourceAsset());
        double warehouseToStoreDistance = model.getRouteLength(request.getSourceAsset(), request.getDestAsset());
        double totalTravelTime = (toWarehouseDistance + warehouseToStoreDistance) / truck.getSpeed();
        //System.out.println("%.3f\tTask #%s : TRANSPORTATION_STARTED. Request #%s; Truck #%s at %s; From %s -> To %s"
        //      .formatted(model.engine().time(), getId(), request.getId(), truck.getId(), truck.getCurrentAsset().getName(), 
        //              request.getSourceAsset().getName(), request.getDestAsset().getName()));
        truck.onTaskStarted(this);
        model.engine().scheduleRelative(totalTravelTime, () -> {
            truck.onTaskCompleted();
            request.setCompletedTime(model.engine().time());
        //  System.out.println("%.3f\tTask #%s : TRANSPORTATION_FINISHED".formatted(model.engine().time(), getId()));
            taskCompletedHandler.accept(this);
        });
    }

}

Uncomment the System.out.println calls in the execute() method to see more debug information.

The algorithm for a truck working with a request is placed in the execute() method:

  1. The beginTime of the task is set by the engine’s current model time.

  2. When the task is executed, its Truck resides, by design of the model, in some Store. So, first the Truck will need to travel to the Warehouse, and then - carry the cargo to the Store. Travel distance is thus a sum of traveling from the current Store to the Warehouse and from the Warehouse to the destination Store.

  3. Call to truck.onTaskStarted(this) marks the Truck as busy. Here we mark the truck as 'busy' for the transportation duration period using the Engine.scheduleRelative() method.

In the end of the transportation time interval (i.e. when the truck should become free), we notify the truck, the transportation request, and the external observer (via the truckReleaseHandler) object that the transportation is over.

The 'external observer' here will be the Dispatcher object that we will create later.

Changes to the Model class

All Truck instances will be created by a Model object.

Add new fields to the Model class:

Model.java, new fields
private List<Truck> trucks = new ArrayList<>();
private List<TransportationRequest> requests = new ArrayList<>();

Add new methods:

Model.java, new methods
public List<Truck> getTrucks() {
    return trucks;
}

public List<TransportationRequest> getRequests() {
    return requests;
}

public void addRequest(TransportationRequest request) {
    requests.add(request);
}

private void initializeTrucks() {
    var initialAssetForTrucks = getStores().get(0);
    for (int i = 0; i < scenario.getTruckCount(); i++) {
        trucks.add(new Truck(String.valueOf(i + 1), String.valueOf(i + 1), scenario.getTruckSpeed(), initialAssetForTrucks, engine()));
    }
}

Update the initializeModelObjects() method:

Model.java, updated initializeModelObjects() method
private void initializeModelObjects() {
    initializeAssets();
    initializeTrucks();
}

Add the following code to the Model class constructor:

Model.java, new code in constructor
requestGenerator.addNewRequestHandler(this::addRequest);

Now the whole model state is stored in the Model class.

Check the result

Run the program. This should appear in the console (the same output as before):

0,000	Request #1 created
1,024	Request #2 created
3,733	Request #3 created
3,871	Request #4 created
3,877	Request #5 created
4,037	Request #6 created
4,789	Request #7 created
7,358	Request #8 created
8,120	Request #9 created
8,197	Request #10 created
9,129	Request #11 created
10,034	Request #12 created
10,803	Request #13 created