feat: add deterministic vehicle assignment demo
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package com.ft546.wcs.demo.center;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class VehicleAssignmentDecision {
|
||||
private final DecisionCode code;
|
||||
private final VehicleSnapshot vehicle;
|
||||
private final String reason;
|
||||
|
||||
private VehicleAssignmentDecision(
|
||||
DecisionCode code,
|
||||
VehicleSnapshot vehicle,
|
||||
String reason
|
||||
) {
|
||||
this.code = code;
|
||||
this.vehicle = vehicle;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public static VehicleAssignmentDecision assigned(
|
||||
VehicleSnapshot vehicle,
|
||||
String reason
|
||||
) {
|
||||
return new VehicleAssignmentDecision(DecisionCode.ASSIGNED, vehicle, reason);
|
||||
}
|
||||
|
||||
public static VehicleAssignmentDecision rejected(
|
||||
DecisionCode code,
|
||||
String reason
|
||||
) {
|
||||
if (code == DecisionCode.ASSIGNED) {
|
||||
throw new IllegalArgumentException("rejected decision cannot be ASSIGNED");
|
||||
}
|
||||
return new VehicleAssignmentDecision(code, null, reason);
|
||||
}
|
||||
|
||||
public DecisionCode getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public Optional<VehicleSnapshot> getVehicle() {
|
||||
return Optional.ofNullable(vehicle);
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ft546.wcs.demo.center;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchTask;
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class VehicleAssignmentService {
|
||||
|
||||
public VehicleAssignmentDecision select(
|
||||
DispatchTask task,
|
||||
List<VehicleSnapshot> vehicles,
|
||||
Map<String, Site> sites
|
||||
) {
|
||||
if (task == null || vehicles == null || sites == null) {
|
||||
throw new IllegalArgumentException("task, vehicles and sites are required");
|
||||
}
|
||||
Site source = sites.get(task.getSourceSite());
|
||||
if (source == null) {
|
||||
return VehicleAssignmentDecision.rejected(
|
||||
DecisionCode.REJECT_INVALID_SITE,
|
||||
"unknown task source site: " + task.getSourceSite()
|
||||
);
|
||||
}
|
||||
|
||||
List<Candidate> candidates = new ArrayList<Candidate>();
|
||||
boolean eligibleWithUnknownSite = false;
|
||||
for (VehicleSnapshot vehicle : vehicles) {
|
||||
if (!isEligible(vehicle)) {
|
||||
continue;
|
||||
}
|
||||
Site current = sites.get(vehicle.getCurrentSite());
|
||||
if (current == null) {
|
||||
eligibleWithUnknownSite = true;
|
||||
continue;
|
||||
}
|
||||
candidates.add(new Candidate(vehicle, current, source));
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
if (eligibleWithUnknownSite) {
|
||||
return VehicleAssignmentDecision.rejected(
|
||||
DecisionCode.REJECT_INVALID_SITE,
|
||||
"eligible vehicle references an unknown current site"
|
||||
);
|
||||
}
|
||||
return VehicleAssignmentDecision.rejected(
|
||||
DecisionCode.HOLD_NO_ELIGIBLE_VEHICLE,
|
||||
"no connected automatic idle vehicle with battery > 30 is available"
|
||||
);
|
||||
}
|
||||
|
||||
Collections.sort(candidates, Candidate.ORDER);
|
||||
Candidate selected = candidates.get(0);
|
||||
return VehicleAssignmentDecision.assigned(
|
||||
selected.vehicle,
|
||||
"vehicle=" + selected.vehicle.getVehicleId()
|
||||
+ ",sameFloorPenalty=" + selected.sameFloorPenalty
|
||||
+ ",floorDistance=" + selected.floorDistance
|
||||
+ ",yDistance=" + selected.yDistance
|
||||
+ ",xDistance=" + selected.xDistance
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isEligible(VehicleSnapshot vehicle) {
|
||||
return vehicle != null
|
||||
&& vehicle.isConnected()
|
||||
&& vehicle.getWorkMode() == 5
|
||||
&& vehicle.getStatus() == 1
|
||||
&& vehicle.isAllowSend()
|
||||
&& vehicle.getBattery() > 30;
|
||||
}
|
||||
|
||||
private static final class Candidate {
|
||||
private static final Comparator<Candidate> ORDER = new Comparator<Candidate>() {
|
||||
@Override
|
||||
public int compare(Candidate left, Candidate right) {
|
||||
int result = Integer.compare(
|
||||
left.sameFloorPenalty,
|
||||
right.sameFloorPenalty
|
||||
);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = Integer.compare(left.floorDistance, right.floorDistance);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = Integer.compare(left.yDistance, right.yDistance);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = Integer.compare(left.xDistance, right.xDistance);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return left.vehicle.getVehicleId().compareTo(right.vehicle.getVehicleId());
|
||||
}
|
||||
};
|
||||
|
||||
private final VehicleSnapshot vehicle;
|
||||
private final int sameFloorPenalty;
|
||||
private final int floorDistance;
|
||||
private final int yDistance;
|
||||
private final int xDistance;
|
||||
|
||||
private Candidate(VehicleSnapshot vehicle, Site current, Site source) {
|
||||
this.vehicle = vehicle;
|
||||
this.sameFloorPenalty = vehicle.getFloor() == source.getFloor() ? 0 : 1;
|
||||
this.floorDistance = Math.abs(vehicle.getFloor() - source.getFloor());
|
||||
this.yDistance = Math.abs(current.getY() - source.getY());
|
||||
this.xDistance = Math.abs(current.getX() - source.getX());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ft546.wcs.demo.model;
|
||||
|
||||
public enum DecisionCode {
|
||||
ASSIGNED,
|
||||
PATH_READY,
|
||||
AVOIDANCE_CREATED,
|
||||
HOLD_NO_ELIGIBLE_VEHICLE,
|
||||
REJECT_INVALID_SITE,
|
||||
HOLD_NO_PATH,
|
||||
HOLD_CHANNEL_BLOCKED,
|
||||
HOLD_PROTOCOL_FAILURE,
|
||||
SENT,
|
||||
COMPLETED
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.ft546.wcs.demo;
|
||||
|
||||
import com.ft546.wcs.demo.center.VehicleAssignmentDecision;
|
||||
import com.ft546.wcs.demo.center.VehicleAssignmentService;
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchTask;
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class VehicleAssignmentServiceTest {
|
||||
private final VehicleAssignmentService service = new VehicleAssignmentService();
|
||||
private Map<String, Site> sites;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
sites = new LinkedHashMap<String, Site>();
|
||||
sites.put("SOURCE", new Site("SOURCE", 1, 10, 10, "PICK"));
|
||||
sites.put("SAME", new Site("SAME", 1, 1, 1, "AISLE-1"));
|
||||
sites.put("NEAR", new Site("NEAR", 2, 10, 9, "AISLE-2"));
|
||||
sites.put("TIE", new Site("TIE", 1, 1, 1, "AISLE-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameFloorVehicleWinsBeforeCloserCrossFloorVehicle() {
|
||||
DispatchTask task = task("SOURCE");
|
||||
VehicleSnapshot crossFloor = vehicle("1", 2, "NEAR", 80, true, true, 5, 1);
|
||||
VehicleSnapshot sameFloor = vehicle("2", 1, "SAME", 80, true, true, 5, 1);
|
||||
|
||||
VehicleAssignmentDecision decision =
|
||||
service.select(task, Arrays.asList(crossFloor, sameFloor), sites);
|
||||
|
||||
assertEquals(DecisionCode.ASSIGNED, decision.getCode());
|
||||
assertEquals("2", decision.getVehicle().get().getVehicleId());
|
||||
assertTrue(decision.getReason().contains("sameFloorPenalty=0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowerVehicleIdBreaksAnExactDistanceTie() {
|
||||
VehicleSnapshot vehicle2 = vehicle("2", 1, "TIE", 80, true, true, 5, 1);
|
||||
VehicleSnapshot vehicle1 = vehicle("1", 1, "SAME", 80, true, true, 5, 1);
|
||||
|
||||
VehicleAssignmentDecision decision =
|
||||
service.select(task("SOURCE"), Arrays.asList(vehicle2, vehicle1), sites);
|
||||
|
||||
assertEquals("1", decision.getVehicle().get().getVehicleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyConnectedAutomaticIdleAllowedAndChargedVehicleIsEligible() {
|
||||
VehicleSnapshot offline = vehicle("1", 1, "SAME", 80, false, true, 5, 1);
|
||||
VehicleSnapshot lowBattery = vehicle("2", 1, "SAME", 30, true, true, 5, 1);
|
||||
VehicleSnapshot manual = vehicle("3", 1, "SAME", 80, true, true, 4, 1);
|
||||
VehicleSnapshot busy = vehicle("4", 1, "SAME", 80, true, true, 5, 2);
|
||||
VehicleSnapshot blocked = vehicle("5", 1, "SAME", 80, true, false, 5, 1);
|
||||
VehicleSnapshot eligible = vehicle("6", 1, "SAME", 31, true, true, 5, 1);
|
||||
|
||||
VehicleAssignmentDecision decision = service.select(
|
||||
task("SOURCE"),
|
||||
Arrays.asList(offline, lowBattery, manual, busy, blocked, eligible),
|
||||
sites
|
||||
);
|
||||
|
||||
assertEquals("6", decision.getVehicle().get().getVehicleId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingTaskSiteIsRejectedInsteadOfUsingZeroCoordinates() {
|
||||
VehicleAssignmentDecision decision =
|
||||
service.select(task("UNKNOWN"), Arrays.asList(
|
||||
vehicle("1", 1, "SAME", 80, true, true, 5, 1)
|
||||
), sites);
|
||||
|
||||
assertEquals(DecisionCode.REJECT_INVALID_SITE, decision.getCode());
|
||||
assertFalse(decision.getVehicle().isPresent());
|
||||
assertTrue(decision.getReason().contains("UNKNOWN"));
|
||||
}
|
||||
|
||||
private DispatchTask task(String sourceSite) {
|
||||
return new DispatchTask(
|
||||
"TASK-1",
|
||||
DispatchTask.TaskType.OUTBOUND,
|
||||
sourceSite,
|
||||
"SOURCE",
|
||||
1,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
private VehicleSnapshot vehicle(
|
||||
String id,
|
||||
int floor,
|
||||
String site,
|
||||
int battery,
|
||||
boolean connected,
|
||||
boolean allowSend,
|
||||
int workMode,
|
||||
int status
|
||||
) {
|
||||
return new VehicleSnapshot(
|
||||
id, floor, site, workMode, status, battery, connected, allowSend, false
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user