Step 2: Datamodel, forklift shape, pallet positions

1. Update the data model

1.1. Create new classes

Go to the '…​datamodel' bundle and open the model/datamodel.ecore file:

Initial datamodel
Figure 1. Initial datamodel

Create a new 'Forklift' EClass

  • In the 'datamodel.ecore' file editor, right-click on the 'datamodel' item and select the 'New Child' → 'EClass' menu item. A new datamodel class is created.

  • In its properties, set its 'Name' to 'Forklift'.

  • Select 'Agent' as the only ESuper type.

Create a new 'Direction' EEnum

  • In the 'datamodel.ecore' file editor, right-click on the 'datamodel' item and select the 'New Child' → 'EEnum' menu item. A new datamodel enumeration is created.

  • In its properties, set its 'Name' to 'Direction'.

  • Right-click the 'Direction' enum and in its context menu choose 'New Child'→ 'EEnum Literal'. Set the literal’s 'Literal' and 'Name' properties to 'IN', and 'Value' property to 0 (zero).

  • In the same manner, add another literal to Direction, with 'Literal' and 'Name' equal to 'OUT' and 'Value' equal to 1.

  • Set Direction enum 'Default Value' property to 'IN (0)'.

Create a new 'DockArea' EClass

  • Create a new datamodel EClass with name='DockArea'

  • Add new fields to EClass 'DockArea':

    • new EReference, with name='scenario' and EType='Scenario';

    • new EAttribute, with name='id', EType='EString' and ID=true;

    • new EAttribute, with name='direction' and EType='Direction';

    • new EReference, with name='storagePlaces', EType='Node' and Upper Bound=-1.

Create new fields in 'Scenario' EClass

Add new fields to the Scenario EClass:

  • new EReference, with name='storagePlaces', EType='Node', and Upper Bound=-1;

  • new EReference, with name='dockAreas', EType='DockArea', Containment=true, Upper Bound=-1, and EOpposite='scenario';

Change the type of 'agents' field from 'Agent' to 'Forklift. Change the name of 'agents' field to 'forklifts'. Move the 'scenario' field from 'Agent' class into 'Forklift' class.

Save the modified 'datamodel.ecore' file.

Check if you have the data model similar to this one:

Updated datamodel
Figure 2. Updated datamodel

1.2. Generate datamodel Java classes

In the 'datamodel.genmodel' file (located next to the 'datamodel.ecore' file) editor, right-click the child Datamodel element and choose 'Generate Model Code'.

1.3. Fix compilation errors caused by the datamodel update

  1. AgentPage: should extend AbstractPage<Forklift, Scenario>, call super(Forklift::getScenario);, use SCENARIO__FORKLIFTS.

  2. ObjectsPart: should use SCENARIO__FORKLIFTS, getForklifts() instead of getAgents().

  3. TreePart: should use SCENARIO__FORKLIFTS.

  4. TreeElementScenario: should call getForklifts() instead of getAgents().

  5. ScenarioModel: should call getForklifts() instead of getAgents()

1.4. Download template scenario

Here you can find the scenario that matches the up-to-date data model and contains some test data: https://github.com/amalgama-llc/warehouse-tutorial/raw/main/scenario/Warehouse_lightweight.xlsx

1.5. Run the simulation

  1. Start the application.

  2. Open the downloaded scenario.

  3. Switch to the simulation mode and run the simulation.

Running the `Warehouse_lightweight.xlsx`
Figure 3. Running the Warehouse_lightweight.xlsx

2. Create Forklift class in the simulation bundle

  1. Locate the Agent class already created in the simulation bundle in the com.company.warehouse.simulation.graph.agents package.

  2. Rename it to Forklift.

  3. Add new fields and methods to the Forklift class:

    Forklift.java
        private boolean loaded;
    
        // User-provided callback to announce completion of the current action
        private Runnable onComplete;
    
    //...
    
        public boolean isLoaded() {
            return loaded;
        }
    
        private void resetAction(Runnable onComplete) {
            cancelMoving();
            this.onComplete = onComplete;
        }
    
        private void finishAction() {
            if (onComplete != null) {
                var callback = onComplete;
                onComplete = null;
                callback.run();
            }
        }
    
        public void moveTo(Node node, Runnable onComplete) {
            resetAction(onComplete);
            moveTo(node, currentVelocity);
        }
  4. Replace the onDestinationReached method:

    Forklift.java
        @Override
        public void onDestinationReached(GeometricGraphPosition<Node, Arc> destPosition) {
            super.onDestinationReached(destPosition);
            finishAction();
        }

3. Update forklift shape

Add ForkliftShape class to the com.amalgamasimulation.warehouse.application.animation package:

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.graph.agents.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, () -> 20.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 forklift.isLoaded() ? 6 : 5;
    }

    private Color cargoColor() {
        return forklift.isLoaded() ? Color.green : Color.black;
    }

}

Update the SimulationPart.onShowModel() to use the new ForkliftShape shape instead of the AgentShape:

SimulationPart.java
// ...
model.getAgents().forEach(agent -> animationView.addShape(new ForkliftShape(agent)));
// ...

Add the missing ForkliftShape import.

In the Model.dispatchAgent() method, replace the last line (agent.moveTo…​) with the following to use the new moveTo() method of the Forklift class that depends on the onComplete callback:

Model.java
// ...
        agent.moveTo(possibleDestinationNodes.get(random.nextInt(possibleDestinationNodes.size())), () -> dispatchAgent(agent));
// ...

Run the simulation:

Forklift shape updated
Figure 4. Forklift shape updated

4. Show pallet positions

4.1. PalletPosition class

Add PalletPosition class to the com.company.warehouse.simulation package (the 'simulation' bundle):

PalletPosition.java
package com.company.warehouse.simulation;

import com.company.warehouse.simulation.graph.Node;

/**
 * A location in the main storage area or in a dock area that can contain at max one pallet.
 */
public class PalletPosition {

    public enum Operation {
        // Pallet is moved into the container
        LOADING,
        // Pallet is moved out of the container
        UNLOADING}

    private final Node node;

    /**
     * Whether a pallet is present here
     */
    private boolean busy;

    public PalletPosition(Node node) {
        this.node = node;
    }

    public Node getNode() {
        return node;
    }

    public boolean isBusy() {
        return busy;
    }

    public boolean isAvailableFor(Operation operation) {
        return (busy && operation == Operation.UNLOADING) || (!busy && operation == Operation.LOADING);
    }

    public void performPalletOperation(Operation operation) {
        if (!isAvailableFor(operation)) {
            throw new IllegalStateException("Cannot " + operation);
        }
        busy = !busy;
    }
}

4.2. DockArea class

Add the new DockArea class to the com.company.warehouse.simulation package (the 'simulation' bundle):

DockArea.java
package com.company.warehouse.simulation;

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

import com.company.warehouse.datamodel.Direction;

public class DockArea {

    /**
     * Whether this dock area is incoming (for unloading) or outgoing (for loading).
     */
    private final Direction direction;
    private List<PalletPosition> palletPositions = new ArrayList<>();

    public DockArea(Direction direction) {
        this.direction = direction;
    }

    public Direction getDirection() {
        return direction;
    }

    public void addPalletPosition(PalletPosition palletPosition) {
        palletPositions.add(palletPosition);
    }
}

4.3. Initialize dock areas and pallet positions

In the Model class, add new fields and a new method:

Model.java
// ...
    protected List<PalletPosition> mainStoragePalletPositions = new ArrayList<>();
    protected Map<Direction, List<DockArea>> dockAreas = new HashMap<>();
    protected Map<Direction, List<PalletPosition>> palletPositionsAtDockAreas = new HashMap<>();
// ...

    public List<PalletPosition> getAllPositions() {
        return Utils.union(mainStoragePalletPositions, palletPositionsAtDockAreas.get(Direction.IN), palletPositionsAtDockAreas.get(Direction.OUT));
    }

Add missing import statements.

In the ScenarioModel class, add new methods:

ScenarioModel.java
    private PalletPosition newPalletPosition(com.company.warehouse.datamodel.Node scenarioNode, boolean busy) {
        var node = mapping.nodesMap.get(scenarioNode);
        var p = new PalletPosition(node);
        if (busy) {
            p.performPalletOperation(Operation.LOADING);
        }
        return p;
    }

    private void initializeMainStorage() {
        scenario.getStoragePlaces()
                .stream()
                .forEach(scenarioNode -> mainStoragePalletPositions.add(newPalletPosition(scenarioNode, randomTrue(0.5))));
    }

    private void initializeDockAreas() {
        for (var direction : Direction.values()) {
            dockAreas.put(direction, new ArrayList<>());
            palletPositionsAtDockAreas.put(direction, new ArrayList<>());
        }
        for (var scenarioDockArea : scenario.getDockAreas()) {
            var dockArea = new DockArea(scenarioDockArea.getDirection());
            var direction = scenarioDockArea.getDirection();
            for (var scenarioNode : scenarioDockArea.getStoragePlaces()) {
                var palletPosition = newPalletPosition(scenarioNode, randomTrue(0.5));
                dockArea.addPalletPosition(palletPosition);
                palletPositionsAtDockAreas.get(direction).add(palletPosition);
            }
            dockAreas.get(direction).add(dockArea);
        }
    }

    private boolean randomTrue(double probability) {
        return random.nextDouble() < probability;
    }

Add missing import statements.

Append two new lines to the ScenarioModel class constructor:

ScenarioModel.java
// ...
    initializeMainStorage();
    initializeDockAreas();

4.4. PalletPositionShape class

Add a new shape class to the com.company.warehouse.application.animation package:

PalletPositionShape.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.company.warehouse.simulation.PalletPosition;

public class PalletPositionShape extends GroupShape {

    public PalletPositionShape(PalletPosition p) {
        super(p.getNode().getPoint());
        withShape(new RectangleShape(() -> new Point(-5, -5), () -> 10.0, () -> 10.0)
                .withLineColor(Color.gray)
                .withFillColor(() -> p.isAvailableFor(PalletPosition.Operation.UNLOADING) ? Color.green : Color.gray)
                );
    }
}

In the SimulationPart.onShowModel() method, add this new line inside the if (model != null) block:

SimulationPart.java
//...
model.getAllPositions().forEach(p -> animationView.addShape(new PalletPositionShape(p)));
//...

Add the missing import statement.

Run the application. Now the pallet position shapes should be used, colored green (with cargo) or gray (without cargo). At this stage, cargo is not moved around the warehouse yet, so the color of each pallet remains unchanged.

Pallet shapes added
Figure 5. Pallet shapes added