Step 2: Introduce a Forklift Entity to the Simulation Model

It’s time to bring life to our static and dull world. Its first inhabitants - forklifts - will move and function within the warehouse graph. Therefore, we will inherit their class from GraphAgent.

1. Add Forklift class to the simulation bundle

  1. In Project Explorer expand bundles / …​warehouse.simulation / src

  2. Right-click on the …​warehouse.simulation package.

  3. From the context menu, choose New / Class

  4. Fill in the New Java Class dialog

    1. Package: append .equipment to place the new class in the …simulation.equipment sub-package.

    2. Name: Forklift

    3. Superclass: GraphAgent<Node, Arc>

  5. The Forklift.java file should appear in a newly created …equipment package

While writing code, you may need to include missing imports. You can use Eclipse IDE’s ‘import …’ quick fix feature to automate this task.
Forklift.java
package com.company.warehouse.simulation.equipment;

import com.amalgamasimulation.engine.Engine;
import com.amalgamasimulation.graphagent.GraphAgent;
import com.amalgamasimulation.graphagent.GeometricGraphPosition;
import com.amalgamasimulation.graphagent.GraphEnvironment;
import com.company.warehouse.simulation.graph.Arc;
import com.company.warehouse.simulation.graph.Node;

/**
 * A forklift simulation.
 *
 * Extends <code>GraphAgent</code> as this thing is about to move around the warehouse graph.
 */
public class Forklift extends GraphAgent<Node, Arc> {

    /**
     * Having object name is handy for monitoring the system state and for debugging.
     */
    private final String name;

    /**
     * A garage location.
     */
    private final Node base;

    /**
     * A movement speed characteristic of this model of equipment.
     */
    private final double velocity;

    /**
     * Amount of time required for loading a pallet.
     */
    private final double loadingTime;

    /**
     * Amount of time required for unloading a pallet.
     */
    private final double unloadingTime;

    /**
     * User provided callback to notify completion of a current action.
     */
    private Runnable onComplete;

    public Forklift(Engine engine, GraphEnvironment<Node, Arc, ?> graphEnvironment, String name, Node base,
            double velocity, double loadingTime, double unloadingTime) {
        super(engine);
        this.name = name;
        this.base = base;
        this.velocity = velocity;
        this.loadingTime = loadingTime;
        this.unloadingTime = unloadingTime;

        // Immerse the forklift to the graph environment
        setGraphEnvironment(graphEnvironment);
        // and place it to the starting node..
        jumpTo(base);
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * Starts the movement from current to specified location.
     *
     * @param node  target location
     * @param onComplete  callback to notify about arrival
     */
    public void moveTo(Node node, Runnable onComplete) {
        resetAction(onComplete);
        moveTo(node, velocity);
    }

    /**
     * Starts the movement from current location to garage.
     *
     * @param onComplete  callback to notify about arrival
     */
    public void moveToBase(Runnable onComplete) {
        moveTo(base, onComplete);
    }

    /**
     * Aborts the action in progress. E.g. stops moving.
     */
    public void cancelCurrentAction() {
        resetAction(null);
    }

    private void resetAction(Runnable onComplete) {
        cancelMoving();
        this.onComplete = onComplete;
    }

    /**
     * This overriden method is called by GraphAgent class when the movement finishes at the destination point.
     */
    @Override
    public void onDestinationReached(GeometricGraphPosition<Node, Arc> node) {
        super.onDestinationReached(node);
        finishAction();
    }

    /**
     * Invoke callback to notify the outer world that the current action is complete.
     * Reset the callback reference.
     */
    private void finishAction() {
        if (onComplete != null) {
            var callback = onComplete;
            onComplete = null;
            callback.run();
        }
    }

    /**
     * It's easier to debug with a meaningful <code>toString()</code> method.
     */
    @Override
    public String toString() {
        return name;
    }

}

2. Add a forklift instance to the simulation model

Now it’s time to spawn our forklift.

Open the …simulation/Model.java source file.

This is a wizard-generated simulation model that initializes itself with the scenario data. Until now, it has built a graph and some kind of generic Agents. Let’s replace them with our self-made forklifts.

  1. Add a forklifts collection.

  2. Create a Forklift instance from the scenario data.

  3. Add it to the forklifts list.

  4. Don’t forget to add the missing imports.

There are two Forklift classes available for import. Make sure you import the correct one: com.company.warehouse.simulation.equipment.Forklift.
Model.java, create forklifts
    private List<Forklift> forklifts = new ArrayList<>();
    public List<Forklift> getForklifts() {
        return forklifts;
    }
...
    private void initializeForklifts() {
        for (var scenarioForklift : scenario.getForklifts()) {
            forklifts.add(new Forklift(
                    engine(),
                    graphEnvironment,
                    scenarioForklift.getName(),
                    mapping.nodesMap.get(scenarioForklift.getBase()),
                    scenarioForklift.getVelocity(),
                    scenarioForklift.getLoadingTimeSec() * second(),
                    scenarioForklift.getUnloadingTimeSec() * second()
                    ));
        }
    }

And now we can give the first assignment to our forklifts.

    private void makeAssignments() {
        var nodesSpliter = graphEnvironment.getNodeValues().spliterator();
        int i = 0;
        for (var forklift : forklifts) {
            final var startTime = i++ * minute();
            nodesSpliter.tryAdvance(targetNode ->
                engine().scheduleRelative(startTime, () -> forklift.moveTo(targetNode, null))
            );
        }
    }

We iterate over all the forklifts and make them start movement one by one over 1-minute intervals. As destination points, we consequentially select nodes from the graph.

Now call both methods from the constructor instead of initializing Agents.

    public Model(Engine engine, Scenario scenario) {
...
//      engine().setTemporal(scenario.getBeginDate(), ChronoUnit.HOURS);
        engine().setTemporal(scenario.getBeginDate(), ChronoUnit.MINUTES);
...
        graphEnvironment = new GraphEnvironment<>();
//      engine.scheduleRelative(0, () -> getAgents().forEach(a -> dispatchAgent(a)));
        initializeNodes();
        initializeArcs();
        initializeForklifts();
        
        makeAssignments();
    }
We updated the call to Engine::setTemporal() in order to set minutes as the main time units for our simulation. Minutes are more convenient than hours for our task.

3. Add some shape to the animation

If you already tried to start the simulation, you might have been disappointed. Nothing remarkable is going on there. That’s because the Forklift is a new entity for the application. So, no one knows how to display it. Let’s give a visible shape to our creation.

  1. In the application bundle, animation package, add the ForkliftShape class derived from GroupShape.

  2. Pass a Forklift to the constructor. At this step, you should also export the equipment package from the simulation bundle. Nicely, Eclipse export quick fix can help you with that.

  3. In the constructor, add two rectangle shapes that will represent a cabin and a fork.

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);
    }

    private double cargoSize() {
        return 5;
    }

    private Color cargoColor() {
        return Color.black;
    }
}
  1. In the parts.simulation package, open the SimulationPart class. It is responsible for the graphical representation of the simulation model.

  2. In the onShowModel() method, replace the line that creates AgentShapes with a similar one that creates ForkliftShape for every forklift in the model.

SimulationPart.java
    private void onShowModel(Model model) {
...
            model.getArcs().forEach(r -> animationView.addShape(new ArcShape(r)));      
            model.getForklifts().forEach(f -> animationView.addShape(new ForkliftShape(f)));
        }
        animationView.adjustWindow();
    }

4. Relaunch the application

  1. Open the scenario from the Warehouse_Scenario.xlsx file.

  2. Start the simulation in Simulation Mode.

At last, our warehouse is alive. Beautiful two-colored rectangles (the avatars of our forklifts) are riding from the “garage” to some meaningless points.

Simulation