Step 8: Trucks, Gates & Dispatching tasks
The forklifts aren’t supposed to be the only inhabitants of our warehouse world. As you remember, the trucks are coming…
1. Trucks
Since trucks can contain pallets and can be loaded or unloaded,
we’ll make them implement the PalletContainer
interface.
package com.company.warehouse.simulation.equipment;
import com.company.warehouse.datamodel.Direction;
import com.company.warehouse.simulation.PalletContainer;
import com.company.warehouse.simulation.graph.Node;
public class Truck implements PalletContainer {
/**
* If this truck is incoming our outgoing.
*/
private final Direction direction;
/**
* How many pallets can fit in the truck.
*/
private final int capacity;
/**
* How many pallets are currently in the truck.
*/
private int palletCount;
/**
* Location where truck is parked.
*/
private Node node;
public Truck(Direction direction, int capacity) {
this.direction = direction;
this.capacity = capacity;
// Incoming trucks are initially full, outgoing are empty
palletCount = direction == Direction.IN ? capacity : 0;
}
public Direction getDirection() {
return direction;
}
public int getCapacity() {
return capacity;
}
public int getPalletCount() {
return palletCount;
}
@Override
public Node getNode() {
if (node == null) {
throw new RuntimeException("Truck is not parked");
}
return node;
}
/**
* Establish parking.
* @param node a parking location node
*/
public void park(Node node) {
if (node == null) {
throw new RuntimeException("Can't park to null");
} else if (this.node != null) {
throw new RuntimeException("Truck is already parked");
}
this.node = node;
}
public void unpark(Node node) {
if (node == null) {
throw new RuntimeException("Can't unpark from null");
} else if (this.node != node) {
throw new RuntimeException("Truck is not parked to this place");
}
this.node = null;
}
@Override
public boolean isAvailableFor(boolean loading) {
return loading ? palletCount < capacity : palletCount > 0;
}
@Override
public void placePallet(boolean loading) {
if (!isAvailableFor(loading)) {
throw new RuntimeException("Truck not available for loading (" + loading + "): " + this);
}
palletCount += loading ? 1 : -1;
}
@Override
public String toString() {
return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode());
}
}
2. Gates
Gates are also of two types: incoming and outgoing. Each gate has an entrance node where a truck can be parked.
package com.company.warehouse.simulation;
import java.util.Objects;
import java.util.Optional;
import com.company.warehouse.datamodel.Direction;
import com.company.warehouse.simulation.equipment.Truck;
import com.company.warehouse.simulation.graph.Node;
public class Gate {
/**
* Whether this gate is incoming or outgoing.
*/
private final Direction direction;
/**
* A node to park trucks at.
*/
private final Node entrance;
/**
* A parked truck.
*/
private Truck truck;
public Gate(Direction direction, Node entrance) {
this.direction = direction;
this.entrance = entrance;
}
public Direction getDirection() {
return direction;
}
public Node getEntrance() {
return entrance;
}
public Optional<Truck> getTruck() {
return Optional.ofNullable(truck);
}
/**
* Parks a truck at this gate.
* @param truck
*/
public void parkTruck(Truck truck) {
Objects.requireNonNull(truck);
if (this.truck != null) {
throw new RuntimeException("Can't park " + truck + " to " + this + " as " + this.truck + " already parked!");
}
this.truck = truck;
truck.park(getEntrance());
}
public void unparkTruck(Truck truck) {
Objects.requireNonNull(truck);
if (this.truck != truck) {
throw new RuntimeException("Can't unpark " + truck + " from " + this + " as another " + this.truck + " parked!");
}
truck.unpark(getEntrance());
this.truck = null;
}
@Override
public String toString() {
return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode());
}
}
Update gate initialization in the Model
.
First, add the gates
field and the getGates()
method:
public class Model extends com.amalgamasimulation.engine.Model {
...
private final Map<Direction, List<Gate>> gatesByDirection = Map.of(
Direction.IN, new ArrayList<>(),
Direction.OUT, new ArrayList<>()
);
...
public List<Gate> getGatesInDirection(Direction direction) {
return gatesByDirection.get(direction);
}
...
Then, replace the initializeGates()
method:
private void initializeGates() {
for (var scenarioGate : scenario.getGates()) {
final var direction = scenarioGate.getDirection();
final var entrance = mapping.nodesMap.get(scenarioGate.getEntrance());
scenarioGate.getPlaces().stream()
.forEach(scenarioNode -> newPalletPosition(scenarioNode, randomTrue(0.5)));
final var gate = new Gate(direction, entrance);
getGatesInDirection(direction).add(gate);
}
}
3. Visualize truck at a gate
The tricky part of the shape class is to turn the truck image so that it correctly docks to the gate.
package com.company.warehouse.application.animation;
import java.awt.Color;
import java.util.Objects;
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.Gate;
import com.company.warehouse.simulation.Model;
import com.company.warehouse.simulation.graph.Node;
public class TruckAtGateShape extends GroupShape {
public TruckAtGateShape(Gate gate, Model model) {
final var entrance = gate.getEntrance();
withVisibility(() -> gate.getTruck().isPresent());
withPoint(entrance.getPoint());
withRotationAngle(arcDirectionFor(entrance, model));
withShape(new RectangleShape(() -> new Point(-6, 0), () -> 12.0, () -> 32.0)
.withFillColor(Colors.lightGrey)
);
withShape(
new RectangleShape(
() -> new Point(-6, 32.0 * (1 - cargoLevel(gate))),
() -> 12.0,
() -> 32.0 * cargoLevel(gate)
).withFillColor(Color.green)
);
withShape(new RectangleShape(() -> new Point(-5, 32), () -> 10.0, () -> 8.0)
.withFillColor(Colors.dodgerBlue)
);
withFixedScale(1);
}
/**
* @param gate
* @return a fraction of capacity in use
*/
private double cargoLevel(Gate gate) {
return gate.getTruck()
.map(t -> t.getPalletCount() / (double) t.getCapacity())
.orElse(0.0);
}
/**
* Calculates the orientation for the truck image.
* @param node a parking location
* @param model warehouse simulation model instance
* @return a direction opposite to the arc connected to the node
*/
private static double arcDirectionFor(Node node, Model model) {
return - Objects.requireNonNull(
model.getGraphEnvironment().getGraphNode(node).getFirstArcIn(),
"No arc comes to the gate entrance node"
).getValue().getPolyline().getLastSegment().getHeading();
}
}
Here we assume that a single arc is connected to the parking node and use its direction to properly direct the truck image.
We display the truck’s load level as a green bar, similar to battery charge level indicators.
We will display trucks at the gates in the animation in a usual way:
private void onShowModel(Model model) {
...
model.getArcs().forEach(r -> animationView.addShape(new ArcShape(r)));
model.getAllPositions().forEach(p -> animationView.addShape(new PalletPositionShape(p)));
model.getForklifts().forEach(f -> animationView.addShape(new ForkliftShape(f)));
for (var d : com.company.warehouse.datamodel.Direction.values()) {
model.getGatesInDirection(d).forEach(g -> animationView.addShape(new TruckAtGateShape(g, model)));
}
}
animationView.adjustWindow();
}
4. Dispatcher: organizing the whole process
Since the variety of entities in our model grows, it’s time to extract the coordination algorithm to a separate class.
The Dispatcher
class will be responsible for orchestrating the whole system.
At this step, we’ll implement the simplest workflow. Forklifts will pick up pallets from incoming trucks and move them directly to outgoing trucks.
package com.company.warehouse.simulation;
import com.amalgamasimulation.engine.Engine;
import com.company.warehouse.datamodel.Direction;
import com.company.warehouse.simulation.equipment.Forklift;
import com.company.warehouse.simulation.equipment.Truck;
import com.company.warehouse.simulation.tasks.MovePalletTask;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
public class Dispatcher {
private final Engine engine;
private final Model model;
private final Queue<Forklift> availableForklifts = new LinkedList<>();
public Dispatcher(Model model) {
engine = model.engine();
this.model = model;
availableForklifts.addAll(model.getForklifts());
}
public void truckArrived(Truck truck) {
final var gate = model.getGatesInDirection(truck.getDirection()).stream()
.filter(g -> g.getTruck().isEmpty())
.findFirst()
.orElseThrow(() -> new RuntimeException("No gate available to park " + truck));
handleTruckOnGate(truck, gate);
}
private void handleTruckOnGate(Truck truck, Gate gate) {
final var direction = gate.getDirection();
final var oppositeDirection = (direction == Direction.IN) ? Direction.OUT : Direction.IN;
gate.parkTruck(truck);
model.getGatesInDirection(oppositeDirection).stream()
.filter(g -> g.getTruck().isPresent())
.findFirst()
.ifPresent(oppositeGate ->
handleGatePair(gate, oppositeGate, Objects.requireNonNull(availableForklifts.poll(), "No available forklifts"))
);
}
private void handleGatePair(Gate gate, Gate oppositeGate, Forklift forklift) {
final var truck = gate.getTruck().get();
final var oppositeTruck = oppositeGate.getTruck().get();
final boolean loading = gate.getDirection() == Direction.OUT;
if (!truck.isAvailableFor(loading)) {
gate.unparkTruck(truck);
oppositeGate.unparkTruck(oppositeTruck);
availableForklifts.add(forklift);
return;
}
newMovePalletTask(forklift, truck, oppositeTruck, loading).start(() ->
handleGatePair(gate, oppositeGate, forklift)
);
}
private MovePalletTask newMovePalletTask(Forklift forklift, PalletContainer a, PalletContainer b, boolean reverse) {
final PalletContainer from = reverse ? b : a;
final PalletContainer to = reverse ? a : b;
return new MovePalletTask(engine, forklift, from, to);
}
}
-
The
truckArrived()
method is called upon each truck arrival. It looks for an available gate and, if any is found, callshandleTruckOnGate()
. -
handleTruckOnGate()
parks the truck at the gate and finds another gate of the opposite type, then takes a forklift and callshandleGatePair()
. -
handleGatePair()
startsMovePalletTask
and on its completion calls itself. This recursion finishes as soon as the truck become completely unloaded (or loaded). In this case both of the trucks are unparked and the forklift is returned to the pool. -
newMovePalletTask
method is used to select the appropriate nodes' order depending on the gate direction.
5. Trucks' arrival events
Let’s implement the truck flow.
public class Model extends com.amalgamasimulation.engine.Model {
...
private final Dispatcher dispatcher;
...
private void spawnTrucks() {
for (var d : Direction.VALUES) {
dispatcher.truckArrived(new Truck(d, scenario.getTruckCapacity()));
}
engine().scheduleRelative(scenario.getTruckArrivalIntervalMin() * minute(), this::spawnTrucks);
}
...
In the constructor, replace the makeAssignments()
call with these 2 lines:
public Model(Engine engine, Scenario scenario) {
...
// makeAssignments();
dispatcher = new Dispatcher(this);
engine().scheduleRelative(0, this::spawnTrucks);
}
Here we introduce the dispatcher
object.
It will take care of the arriving trucks.
The spawnTrucks()
method creates two trucks: incoming and outgoing.
We request the engine
to call spawnTrucks()
at the beginning of the simulation.
This method reschedules itself upon each invocation.
Thus, we’ll get a couple of trucks regularly at the interval provided in the scenario.
6. Let’s see how this works
Relaunch the app and start the simulation.
The forklifts start to move cargo from the incoming trucks to the outgoing ones.

But…
Very soon, the simulation stops, and we can see an error in the Eclipse console: java.lang.RuntimeException: No gate available to park Truck

Surely you expected something like this. Our dispatching algorithm is too naive. It claims resources and doesn’t bother about what will happen if there aren’t enough of them.
Actually, we have 2 types of resources, the lack of which can break the algorithm:
-
Gates available for parking
-
Forklifts available for moving pallets
It seems that we should develop a more sophisticated approach that will deal with the limited resources more carefully…