Step 3: Move cargo

1. Tasks

1.1. Add and export tasks package

  1. In the 'simulation' bundle, create a new com.company.warehouse.simulation.tasks package.

  2. Open the META-INF/MANIFEST.MF file in the simulation bundle.

  3. Open its 'Runtime' tab.

  4. Make sure that the com.company.warehouse.simulation.tasks is in the list of exported packages.

1.2. IEquipmentState

Add the IEquipmentState interface to the 'simulation' bundle. It will be the parent of forklift states. Its only method indicates whether an equipment item is being utilized when in this state, so that we can calculate the overall utilization.

IEquipmentState.java
package com.company.warehouse.simulation.tasks;

public interface IEquipmentState {

    boolean isUtilized();

}

1.3. MovePalletTask

Create the MovePalletTask class:

MovePalletTask.java
package com.company.warehouse.simulation.task;

import com.amalgamasimulation.engine.Engine;
import com.amalgamasimulation.engine.StateMachine;
import com.company.warehouse.simulation.PalletPosition;
import com.company.warehouse.simulation.graph.agents.Forklift;

/**
 * A task for one forklift to move a pallet from its current position to another.
 */
public class MovePalletTask {

    /**
     * All the possible states for the <code>StateMachine</code>.
     */
    public enum State implements IEquipmentState {
        PENDING(false), // not utilizing
        MOVE_TO_LOADING(true),
        LOADING(true),
        MOVE_TO_UNLOADING(true),
        UNLOADING(true),
        FINISHED(false), // not utilizing
        ;

        private boolean utilized;
        @Override
        public boolean isUtilized() {
            return utilized;
        }

        private State(boolean utilized) {
            this.utilized = utilized;
        }
    }

    private final Forklift forklift;
    private final PalletPosition from;
    private final PalletPosition to;
    private final StateMachine<State> control;

    /**
     * User-provided task completion callback
     */
    private Runnable onComplete;

    public MovePalletTask(Engine engine, Forklift forklift, PalletPosition from, PalletPosition to) {
        this.forklift = forklift;
        this.from = from;
        this.to = to;
        // Create a StateMachine providing all the states and specifying a starting one.
        control = new StateMachine<>(State.values(), State.PENDING, engine)
                // Declare all allowed transitions
                .addTransition(State.PENDING, State.MOVE_TO_LOADING)
                .addTransition(State.MOVE_TO_LOADING, State.LOADING)
                .addTransition(State.LOADING, State.MOVE_TO_UNLOADING)
                .addTransition(State.MOVE_TO_UNLOADING, State.UNLOADING)
                .addTransition(State.UNLOADING, State.FINISHED)
                // Define state handlers
                .addEnterAction(State.MOVE_TO_LOADING, state -> moveToLoading())
                .addEnterAction(State.LOADING, state -> loading())
                .addEnterAction(State.MOVE_TO_UNLOADING, state -> moveToUnloading())
                .addEnterAction(State.UNLOADING, state -> unloading())
                .addEnterAction(State.FINISHED, state -> finished())
                ;
    }

    /**
     * Starts the task.
     * @param onComplete  callback to invoke on completion
     */
    public void start(Runnable onComplete) {
        this.onComplete = onComplete != null ? onComplete : () -> {};
        control.receiveMessage(State.MOVE_TO_LOADING);
    }

    private void moveToLoading() {
        forklift.moveTo(from.getNode(),
                () -> control.receiveMessage(State.LOADING));
    }

    private void loading() {
        forklift.load(from,
                () -> control.receiveMessage(State.MOVE_TO_UNLOADING));
    }

    private void moveToUnloading() {
        forklift.moveTo(to.getNode(),
                () -> control.receiveMessage(State.UNLOADING));
    }

    private void unloading() {
        forklift.unload(to,
                () -> control.receiveMessage(State.FINISHED));
    }

    private void finished() {
        onComplete.run();
    }
}

Don’t worry: this class contains some compilation errors that will soon be fixed by the upcoming changes.

1.4. Update the Forklift class ('simulation' bundle)

Add the following fields and methods:

Forklift.java
    private MovePalletTask task;

    //...

    public MovePalletTask getCurrentTask() {
        return task;
    }

    public void setCurrentTask(MovePalletTask task) {
        this.task = task;
    }

    public boolean isIdle() {
        return task == null;
    }

    public void load(PalletPosition container, Runnable onComplete) {
        resetAction(onComplete);
        loaded = true;
        container.performPalletOperation(Operation.UNLOADING);
        finishAction();
    }

    public void unload(PalletPosition container, Runnable onComplete) {
        resetAction(onComplete);
        loaded = false;
        container.performPalletOperation(Operation.LOADING);
        finishAction();
    }

Add the missing import statements.

1.5. Update the Model class

Add the dispatchIdleForklifts() method:

Model.java
    private void dispatchIdleForklifts() {
        engine().scheduleRelative(0, () -> getAgents()  .stream()
                                                        .filter(Forklift::isIdle)
                                                        .forEach(this::dispatchAgent));
    }

Replace the dispatchAgent() method with the following field and two methods:

Model.java
    // pallet positions that are used in currently running tasks
    private Set<PalletPosition> palletPositionsInTasks = new HashSet<>();

    private void dispatchAgent(Forklift forklift) {
        if (tryDispatchAgent(forklift, palletPositionsAtDockAreas.get(Direction.IN), mainStoragePalletPositions)) {
            return;
        }
        if (tryDispatchAgent(forklift, mainStoragePalletPositions, palletPositionsAtDockAreas.get(Direction.OUT))) {
            return;
        }
        if (tryDispatchAgent(forklift, palletPositionsAtDockAreas.get(Direction.IN), palletPositionsAtDockAreas.get(Direction.OUT))) {
            return;
        }
    }

    private boolean tryDispatchAgent(Forklift forklift, List<PalletPosition> possibleSources, List<PalletPosition> possibleDestinations) {
        possibleSources = possibleSources   .stream()
                                            .filter(pp -> !palletPositionsInTasks.contains(pp))
                                            .filter(pp -> pp.isAvailableFor(Operation.UNLOADING))
                                            .toList();
        possibleDestinations = possibleDestinations .stream()
                                                    .filter(pp -> !palletPositionsInTasks.contains(pp))
                                                    .filter(pp -> pp.isAvailableFor(Operation.LOADING))
                                                    .toList();
        if (possibleSources.isEmpty() || possibleDestinations.isEmpty()) {
            return false;
        }
        var from = possibleSources.get(random.nextInt(possibleSources.size()));
        var to = possibleDestinations.get(random.nextInt(possibleDestinations.size()));
        var movingTask = new MovePalletTask(engine(), forklift, from, to);
        // starting the task
        palletPositionsInTasks.add(from);
        palletPositionsInTasks.add(to);
        forklift.setCurrentTask(movingTask);
        movingTask.start(() -> {
            palletPositionsInTasks.remove(from);
            palletPositionsInTasks.remove(to);
            forklift.setCurrentTask(null);
            dispatchIdleForklifts();
        });
        return true;
    }

Add the missing import statements.

Run the application. Now the forklifts should move cargo around the warehouse.

Forklifts move with cargo
Figure 1. Forklifts move with cargo

2. Update Gantt chart

In the com.company.warehouse.simulation.graph.agents.stats.slots package, remove the BypassingSlot and MovingSlot classes.

Then, update the AgentStatsSlot class:

AgentStatsSlot.java
package com.company.warehouse.simulation.graph.agents.stats.slots;

import com.amalgamasimulation.core.scheduling.Slot;
import com.company.warehouse.simulation.graph.agents.Forklift;
import com.company.warehouse.simulation.tasks.IEquipmentState;

public class AgentStatsSlot extends Slot {

    private boolean closed;
    private Forklift agent;
    private IEquipmentState state;

    public AgentStatsSlot(double beginTime, Forklift agent, IEquipmentState state) {
        super(beginTime, beginTime);
        this.agent = agent;
        this.state = state;
    }

    @Override
    public double endTime() {
        return Math.max(beginTime(), closed ? super.endTime() : agent.time());
    }

    public void close() {
        max = agent.time();
        closed = true;
    }

    public IEquipmentState getState() {
        return state;
    }
}

The Forklift class now contains some compilation errors due to the removal of the BypassingSlot and MovingSlot classes.

In the Forklift class:

  1. Remove the AgentState enum.

  2. Remove the control field and its initialization in the constructor.

  3. Remove the onEnteredState(MOVING, engine); call from the constructor (the last line).

  4. Remove the onOtherAgentReached(), onBypassingEntered(), onMovingEntered(), onEnteredState() methods.

Add the following methods and a field:

Forklift.java
    public void putStatsSlot(IEquipmentState state) {
        var newSlot = new AgentStatsSlot(engine.time(), this, state);
        if(!statsSlots.isEmpty()) {
            var lastSlot = statsSlots.get(statsSlots.size() - 1);
            if(lastSlot.duration() > 0) {
                lastSlot.close();
                statsSlots.add(newSlot);
            } else {
                statsSlots.set(statsSlots.size() - 1, newSlot);
            }
        } else {
            statsSlots.add(newSlot);
        }

        utilization = Utils.zidz(
                statsSlots.stream().filter(slot -> slot.getState().isUtilized()).mapToDouble(slot -> slot.duration()).sum(),
                time());
    }

    private double utilization = 0;

    public double getUtilization() {
        return utilization;
    }

Add the missing import statements. The Forklift class should now compile.

In the MovePalletTask class constructor, add the following command to the control initialization:

MovePalletTask.java
//...
.addEnterAction((state, message) -> forklift.putStatsSlot(state))
//...

Switch to the 'application' bundle and add the new EquipmentStateUI class:

EquipmentStateUI.java
package com.company.warehouse.application;

import java.awt.Color;
import java.util.Map;

import com.amalgamasimulation.utils.Colors;
import com.company.warehouse.simulation.tasks.IEquipmentState;
import com.company.warehouse.simulation.tasks.MovePalletTask;

public class EquipmentStateUI {

    public static String nameOf(IEquipmentState state) {
        return names.get(state);
    }

    public static Color colorOf(IEquipmentState state) {
        return colors.get(state);
    }

    private static Map<IEquipmentState, String> names = Map.of(
            MovePalletTask.State.PENDING, "-",
            MovePalletTask.State.MOVE_TO_LOADING, "Move to loading",
            MovePalletTask.State.LOADING, "Loading",
            MovePalletTask.State.MOVE_TO_UNLOADING, "Move to unloading",
            MovePalletTask.State.UNLOADING, "Unloading",
            MovePalletTask.State.FINISHED, "-"
            );

    private static Map<IEquipmentState, Color> colors = Map.of(
            MovePalletTask.State.PENDING, Colors.gray,
            MovePalletTask.State.MOVE_TO_LOADING, Colors.orange,
            MovePalletTask.State.LOADING, Colors.blue,
            MovePalletTask.State.MOVE_TO_UNLOADING, Colors.orangeRed,
            MovePalletTask.State.UNLOADING, Colors.green,
            MovePalletTask.State.FINISHED, Colors.gray
            );

}

Replace the GanttChartPart.updateContent() method:

GanttChartPart.java
    private void updateContent(Model model) {
        ganttChart.getVisualSetContainer().clear();
        if (model != null) {
            ganttChart.getXAxis().setTimeStyle(AxisTimeStyle.getDefault(model.timeToDate(0), model.timeUnit()))
                .setDisplayedRange(0,  model.dateToTime(model.timeToDate(model.getEndTime())));

            model.getAgents().forEach(forklift -> {

                var visualSet = new GanttVisualSet<>(forklift.getName(), () -> forklift.getStatsSlots(), t -> t.beginTime(), t -> t.endTime())
                        .setBackgroundColor(s -> EquipmentStateUI.colorOf(s.getState()))
                    .setLabelText( LabelSide.TOP_LEFT, this::getTopLeftText, s -> 9.0, s -> Colors.white )
                    .setLabelText( LabelSide.TOP_CENTER, this::getTopCenterText, s -> 9.0, s -> Colors.white )
                    .setLabelText( LabelSide.TOP_RIGHT, this::getTopRightText, s -> 9.0, s -> Colors.white )
                    .setLabelText( LabelSide.MIDDLE_CENTER, this::getMiddleCenterText, s -> 12.0, s -> Colors.white );
                ganttChart.getVisualSetContainer().addVisualSet(visualSet);
            });
        }
        ganttChart.redraw();
    }

Add the missing import statements.

Run the simulation and open the 'Gantt Chart' part:

Gantt chart with updated slots
Figure 2. Gantt chart with updated slots