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:
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:
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
-
AgentPage
: should extendAbstractPage<Forklift, Scenario>
, callsuper(Forklift::getScenario);
, useSCENARIO__FORKLIFTS
. -
ObjectsPart
: should useSCENARIO__FORKLIFTS
,getForklifts()
instead ofgetAgents()
. -
TreePart
: should useSCENARIO__FORKLIFTS
. -
TreeElementScenario
: should callgetForklifts()
instead ofgetAgents()
. -
ScenarioModel
: should callgetForklifts()
instead ofgetAgents()
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
2. Create Forklift
class in the simulation
bundle
-
Locate the
Agent
class already created in thesimulation
bundle in thecom.company.warehouse.simulation.graph.agents
package. -
Rename it to
Forklift
. -
Add new fields and methods to the
Forklift
class:Forklift.javaprivate 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); }
-
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:
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
:
// ...
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:
// ...
agent.moveTo(possibleDestinationNodes.get(random.nextInt(possibleDestinationNodes.size())), () -> dispatchAgent(agent));
// ...
Run the simulation:
4. Show pallet positions
4.1. PalletPosition
class
Add PalletPosition
class to the com.company.warehouse.simulation
package (the 'simulation' bundle):
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):
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:
// ...
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:
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:
// ...
initializeMainStorage();
initializeDockAreas();
4.4. PalletPositionShape
class
Add a new shape class to the com.company.warehouse.application.animation
package:
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:
//...
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.