Step 5: Cargo Moving Task

In order to manage complex systems like our warehouse, we have to coordinate a variety of tasks. Let’s implement the simplest one - the movement of cargo from one location to another.

1. Forklift loading/unloading

To accomplish this, we need to upgrade our forklifts. So far, they are only capable of roaming around the warehouse. But the purpose of a forklift is to move cargo. Since a real forklift is able to pick up, carry, and drop stuff, we have to implement this.

Let’s equip our forklifts with loading and unloading actions, as well as with a state of being loaded or not loaded.

Add the following code to the Forklift class, along with appropriate imports.

Forklift.java
...
    private boolean loaded;
    public boolean isLoaded() {
        return loaded;
    }
...
    public void load(PalletContainer container, Runnable onComplete) {
        resetAction(onComplete);
        engine.scheduleRelative(loadingTime, () -> {
            loaded = true;
            container.placePallet(false);
            finishAction();
        });
    }

    public void unload(PalletContainer container, Runnable onComplete) {
        resetAction(onComplete);
        engine.scheduleRelative(unloadingTime, () -> {
            loaded = false;
            container.placePallet(true);
            finishAction();
        });
    }
...

Yes. Just that simple. We simulate loading and unloading just by wasting an appropriate amount of time and updating the state of the forklift and the container after that.

Let’s update ForkliftShape to reflect the new forklift’s loaded state:

ForkliftShape.java
package com.company.warehouse.application.animation;

import java.awt.Color;

import com.amalgamasimulation.animation.shapes.shapes2d.GroupShape;
import com.amalgamasimulation.animation.shapes.shapes2d.RectangleShape;
import com.amalgamasimulation.geometry.Point;
import com.amalgamasimulation.utils.Colors;
import com.company.warehouse.simulation.equipment.Forklift;

public class ForkliftShape extends GroupShape {

    private Forklift forklift;

    public ForkliftShape(Forklift forklift) {
        super(() -> forklift.getCurrentAnimationPoint());
        this.forklift = forklift;
        withShape(new RectangleShape(() -> new Point(-5, -5), () -> 10.0, () -> 10.0)
                .withFillColor(Colors.orange)
                );
        withShape(new RectangleShape(() -> new Point(-cargoSize(), 10 - cargoSize()), () -> cargoSize() * 2, () -> cargoSize() * 2)
                .withFillColor(() -> cargoColor())
                );
        withRotationAngle(() -> - forklift.getCurrentAnimationHeading());
        withFixedScale(1);
    }

    // tag::loaded[]
    private double cargoSize() {
        return forklift.isLoaded() ? 6 : 5;
    }

    private Color cargoColor() {
        return forklift.isLoaded() ? Color.green : Color.black;
    }
    // end::loaded[]
}

As you see, we can use a Java Supplier instead of a constant in order to visualize something changing during the simulation process.

Now our forklifts are ready for their new duties.

2. Base task classes

Let’s design a common base for all the types of tasks. What do we expect from any task? Most probably, we’ll want to be able to start a task, and we may want to be notified when the task completes.

Create an abstract class Task in the …simulation.tasks package.

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

import com.amalgamasimulation.engine.Engine;

public abstract class Task {

    protected final Engine engine;
    public Engine getEngine() {
        return engine;
    }
    /**
     * User provided callback to notify upon completion of a current task.
     */
    private Runnable onComplete;

    public Task(Engine engine) {
        this.engine = engine;
    }

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

    /**
     * Override this method to implement a specific task.
     */
    protected abstract void run();

    /**
     * Call this method when task finishes.
     */
    protected void finish() {
        onComplete.run();
    }

}

Tasks may need access to the simulation engine, so we added it as a field in advance.

A slightly more specific ForkliftTask abstract class is for the tasks that will use a forklift.

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

import com.amalgamasimulation.engine.Engine;
import com.company.warehouse.simulation.equipment.Forklift;

public abstract class ForkliftTask extends Task {

    protected final Forklift forklift;

    public ForkliftTask(Engine engine, Forklift forklift) {
        super(engine);
        this.forklift = forklift;
    }

}

3. States and state machine

The process of performing a complex task can be described as switching between different states. Such systems could be conveniently modeled using a State Machine.

To create a State Machine, we’ll need to provide a number of possible states. Since we may need to have another set of states for another type of tasks, let’s make a common interface for all of them:

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

public interface IEquipmentState {

    boolean isUtilized();

}

Using states will also allow us to monitor the process and collect statistical data for analysis. The isUtilized() method will inform us if a state should be considered as utilizing an equipment unit.

4. MovePalletTask

At last, we are ready to derive a non-abstract MovePalletTask class from ForkliftTask.

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

import com.amalgamasimulation.engine.Engine;
import com.amalgamasimulation.engine.StateMachine;
import com.company.warehouse.simulation.PalletContainer;
import com.company.warehouse.simulation.equipment.Forklift;
import com.company.warehouse.simulation.equipment.IEquipmentState;

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

    /**
     * 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 PalletContainer from;
    private final PalletContainer to;
    private final StateMachine<State> control;

    public MovePalletTask(Engine engine, Forklift forklift, PalletContainer from, PalletContainer to) {
        super(engine, 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())
                ;
    }

    /**
     * An entry point for any task.
     */
    @Override
    public void run() {
        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() {
        finish();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode()) + " [from=" + from + ", to=" + to + "]";
    }

}

Here, the usage of the State Machine is pretty straightforward.

To initialize a SM, we define states, transitions, and state entry handlers.

The execution of the task goes this way:

  1. At the beginning of the task life cycle, the SM is in the PENDING state.

  2. As soon as the task starts, in the run() method, we send a message to the SM, simply requesting it to switch to the MOVE_TO_LOADING state. Since we have declared this transition, the SM will satisfy that request.

  3. As soon as it switches to MOVE_TO_LOADING, it will call the moveToLoading() handler. This method instructs the forklift to start moving to the specified location.

  4. When the location is reached, we’ll request the SM to switch to the next state - LOADING.

  5. The same way, each handler initiates a corresponding forklift action and requests the consequent state upon completion.

5. Update the model

To see what we have achieved, let’s assign a task to the forklifts in the Model.

Update Model::makeAssignments() method:

Model.java
...
    private void makeAssignments() {
        // To iterate busy positions
        var busyPositions = graphEnvironment.getNodeValues().stream()
                .flatMap(node -> node.getPalletPosition().stream())
                .filter(position -> position.isBusy())
                .spliterator();
        
        // To iterate free positions
        var freePositions = graphEnvironment.getNodeValues().stream()
                .flatMap(node -> node.getPalletPosition().stream())
                .filter(position -> !position.isBusy())
                .spliterator();
        
        int i = 0;
        for (var forklift : forklifts) {
            final var startTime = i++ * minute();
            busyPositions.tryAdvance(from ->
                freePositions.tryAdvance(to -> {
                    final var movingTask = new MovePalletTask(engine(), forklift, from, to);
                    engine().scheduleRelative(startTime,
                            () -> movingTask.start(null)
                    );
                })
            );
        }
    }
...

Add appropriate import statements.

Let’s run a simulation and see how gracefully forklifts move cargo to a new location and… after completing the task, they get stuck wherever they happen to be.

Forklifts move with cargo
Figure 1. Forklifts move with cargo