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.
...
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:
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.
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.
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:
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
.
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:
-
At the beginning of the task life cycle, the SM is in the
PENDING
state. -
As soon as the task starts, in the
run()
method, we send a message to the SM, simply requesting it to switch to theMOVE_TO_LOADING
state. Since we have declared this transition, the SM will satisfy that request. -
As soon as it switches to
MOVE_TO_LOADING
, it will call themoveToLoading()
handler. This method instructs the forklift to start moving to the specified location. -
When the location is reached, we’ll request the SM to switch to the next state -
LOADING
. -
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:
...
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.
