feat: simulate wcs traffic control
This commit is contained in:
@@ -60,6 +60,33 @@ public final class DispatchQueueEntry {
|
||||
);
|
||||
}
|
||||
|
||||
public static DispatchQueueEntry avoidance(
|
||||
String queueId,
|
||||
String vehicleId,
|
||||
String startSite,
|
||||
String endSite
|
||||
) {
|
||||
if (queueId == null || queueId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("queue id must not be blank");
|
||||
}
|
||||
if (vehicleId == null || vehicleId.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("vehicle id must not be blank");
|
||||
}
|
||||
if (startSite == null || startSite.trim().isEmpty()
|
||||
|| endSite == null || endSite.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("avoidance sites must not be blank");
|
||||
}
|
||||
return new DispatchQueueEntry(
|
||||
queueId,
|
||||
queueId,
|
||||
vehicleId,
|
||||
QueueType.AVOIDANCE,
|
||||
startSite,
|
||||
endSite,
|
||||
9
|
||||
);
|
||||
}
|
||||
|
||||
public void markIssued(List<String> issuedPath) {
|
||||
if (issued != 0) {
|
||||
throw new IllegalStateException("queue has already been issued");
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GridMap {
|
||||
private final Map<String, Site> sites = new LinkedHashMap<String, Site>();
|
||||
private final Map<String, Set<String>> neighbors =
|
||||
new LinkedHashMap<String, Set<String>>();
|
||||
private final Set<String> blockedSites = new LinkedHashSet<String>();
|
||||
private final Map<String, String> cacheSitesByChannel =
|
||||
new LinkedHashMap<String, String>();
|
||||
|
||||
public void addSite(Site site) {
|
||||
if (site == null) {
|
||||
throw new IllegalArgumentException("site must not be null");
|
||||
}
|
||||
if (sites.containsKey(site.getId())) {
|
||||
throw new IllegalArgumentException("duplicate site: " + site.getId());
|
||||
}
|
||||
sites.put(site.getId(), site);
|
||||
neighbors.put(site.getId(), new LinkedHashSet<String>());
|
||||
}
|
||||
|
||||
public void connect(String left, String right) {
|
||||
requireSite(left);
|
||||
requireSite(right);
|
||||
neighbors.get(left).add(right);
|
||||
neighbors.get(right).add(left);
|
||||
}
|
||||
|
||||
public void block(String siteId) {
|
||||
requireSite(siteId);
|
||||
blockedSites.add(siteId);
|
||||
}
|
||||
|
||||
public void setCacheSite(String channel, String siteId) {
|
||||
if (channel == null || channel.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("channel must not be blank");
|
||||
}
|
||||
requireSite(siteId);
|
||||
cacheSitesByChannel.put(channel, siteId);
|
||||
}
|
||||
|
||||
public Site getSite(String siteId) {
|
||||
return sites.get(siteId);
|
||||
}
|
||||
|
||||
public Map<String, Site> getSites() {
|
||||
return Collections.unmodifiableMap(sites);
|
||||
}
|
||||
|
||||
public List<String> getNeighbors(String siteId) {
|
||||
requireSite(siteId);
|
||||
List<String> result = new ArrayList<String>(neighbors.get(siteId));
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<String> getBlockedSites() {
|
||||
return Collections.unmodifiableSet(blockedSites);
|
||||
}
|
||||
|
||||
public String getCacheSite(String channel) {
|
||||
return cacheSitesByChannel.get(channel);
|
||||
}
|
||||
|
||||
private void requireSite(String siteId) {
|
||||
if (!sites.containsKey(siteId)) {
|
||||
throw new IllegalArgumentException("unknown site: " + siteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class GridPathPlanner {
|
||||
|
||||
public List<String> shortestPath(
|
||||
GridMap map,
|
||||
String start,
|
||||
String end,
|
||||
Set<String> dynamicBlocked
|
||||
) {
|
||||
if (map == null || map.getSite(start) == null || map.getSite(end) == null) {
|
||||
throw new IllegalArgumentException("map, start and end sites are required");
|
||||
}
|
||||
|
||||
Set<String> blocked = new LinkedHashSet<String>(map.getBlockedSites());
|
||||
if (dynamicBlocked != null) {
|
||||
blocked.addAll(dynamicBlocked);
|
||||
}
|
||||
blocked.remove(start);
|
||||
if (blocked.contains(end)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Deque<String> open = new ArrayDeque<String>();
|
||||
Set<String> visited = new HashSet<String>();
|
||||
Map<String, String> parent = new HashMap<String, String>();
|
||||
open.add(start);
|
||||
visited.add(start);
|
||||
|
||||
while (!open.isEmpty()) {
|
||||
String current = open.removeFirst();
|
||||
if (current.equals(end)) {
|
||||
return buildPath(parent, end);
|
||||
}
|
||||
for (String neighbor : map.getNeighbors(current)) {
|
||||
if (!blocked.contains(neighbor) && visited.add(neighbor)) {
|
||||
parent.put(neighbor, current);
|
||||
open.addLast(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<String> buildPath(Map<String, String> parent, String end) {
|
||||
List<String> reversed = new ArrayList<String>();
|
||||
String current = end;
|
||||
while (current != null) {
|
||||
reversed.add(current);
|
||||
current = parent.get(current);
|
||||
}
|
||||
Collections.reverse(reversed);
|
||||
return Collections.unmodifiableList(reversed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||
import com.ft546.wcs.demo.model.Site;
|
||||
import com.ft546.wcs.demo.model.VehicleSnapshot;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TrafficController {
|
||||
private final GridMap map;
|
||||
private final GridPathPlanner planner;
|
||||
|
||||
public TrafficController(GridMap map, GridPathPlanner planner) {
|
||||
if (map == null || planner == null) {
|
||||
throw new IllegalArgumentException("map and planner are required");
|
||||
}
|
||||
this.map = map;
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
public TrafficDecision plan(
|
||||
VehicleSnapshot vehicle,
|
||||
DispatchQueueEntry queue,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
requireInputs(vehicle, queue, vehicles, queues);
|
||||
Site start = map.getSite(vehicle.getCurrentSite());
|
||||
Site target = map.getSite(queue.getEndSite());
|
||||
if (start == null || target == null) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.REJECT_INVALID_SITE,
|
||||
"unknown start or target site"
|
||||
);
|
||||
}
|
||||
|
||||
VehicleSnapshot channelBlocker =
|
||||
findChannelBlocker(vehicle, target, vehicles);
|
||||
if (channelBlocker != null) {
|
||||
if (hasActiveQueue(channelBlocker.getVehicleId(), queues)) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_CHANNEL_BLOCKED,
|
||||
"channel " + target.getChannel() + " is occupied by vehicle "
|
||||
+ channelBlocker.getVehicleId()
|
||||
);
|
||||
}
|
||||
String cacheSite = map.getCacheSite(target.getChannel());
|
||||
if (cacheSite == null) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_CHANNEL_BLOCKED,
|
||||
"channel is blocked and has no configured cache site"
|
||||
);
|
||||
}
|
||||
DispatchQueueEntry avoidance = DispatchQueueEntry.avoidance(
|
||||
"AVOID-" + channelBlocker.getVehicleId() + "-FOR-" + queue.getQueueId(),
|
||||
channelBlocker.getVehicleId(),
|
||||
channelBlocker.getCurrentSite(),
|
||||
cacheSite
|
||||
);
|
||||
return TrafficDecision.avoidance(
|
||||
avoidance,
|
||||
"move vehicle " + channelBlocker.getVehicleId()
|
||||
+ " to cache site " + cacheSite
|
||||
);
|
||||
}
|
||||
|
||||
Set<String> blocked = buildBlockedSites(vehicle, vehicles, queues, queue);
|
||||
List<String> path = planner.shortestPath(
|
||||
map,
|
||||
vehicle.getCurrentSite(),
|
||||
queue.getEndSite(),
|
||||
blocked
|
||||
);
|
||||
if (path.isEmpty()) {
|
||||
return TrafficDecision.hold(
|
||||
DecisionCode.HOLD_NO_PATH,
|
||||
"no path from " + vehicle.getCurrentSite() + " to " + queue.getEndSite()
|
||||
);
|
||||
}
|
||||
return TrafficDecision.path(path);
|
||||
}
|
||||
|
||||
private Set<String> buildBlockedSites(
|
||||
VehicleSnapshot vehicle,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues,
|
||||
DispatchQueueEntry currentQueue
|
||||
) {
|
||||
Set<String> blocked = new LinkedHashSet<String>();
|
||||
for (VehicleSnapshot other : vehicles) {
|
||||
if (!other.getVehicleId().equals(vehicle.getVehicleId())
|
||||
&& !other.isConnected()) {
|
||||
blocked.add(other.getCurrentSite());
|
||||
}
|
||||
}
|
||||
for (DispatchQueueEntry otherQueue : queues) {
|
||||
if (otherQueue.getQueueId().equals(currentQueue.getQueueId())
|
||||
|| otherQueue.getVehicleId().equals(vehicle.getVehicleId())
|
||||
|| otherQueue.getPath().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
VehicleSnapshot owner = findVehicle(otherQueue.getVehicleId(), vehicles);
|
||||
int startIndex = owner == null
|
||||
? 0
|
||||
: otherQueue.getPath().indexOf(owner.getCurrentSite());
|
||||
if (startIndex < 0) {
|
||||
startIndex = 0;
|
||||
}
|
||||
blocked.addAll(
|
||||
otherQueue.getPath().subList(startIndex, otherQueue.getPath().size())
|
||||
);
|
||||
}
|
||||
return blocked;
|
||||
}
|
||||
|
||||
private VehicleSnapshot findChannelBlocker(
|
||||
VehicleSnapshot vehicle,
|
||||
Site target,
|
||||
Collection<VehicleSnapshot> vehicles
|
||||
) {
|
||||
for (VehicleSnapshot other : vehicles) {
|
||||
if (other.getVehicleId().equals(vehicle.getVehicleId())
|
||||
|| !other.isConnected()
|
||||
|| other.getFloor() != target.getFloor()
|
||||
|| other.getWorkMode() != 5
|
||||
|| other.getStatus() != 1) {
|
||||
continue;
|
||||
}
|
||||
Site otherSite = map.getSite(other.getCurrentSite());
|
||||
if (otherSite != null
|
||||
&& target.getChannel().equals(otherSite.getChannel())
|
||||
&& !otherSite.getId().equals(vehicle.getCurrentSite())) {
|
||||
return other;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasActiveQueue(
|
||||
String vehicleId,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
for (DispatchQueueEntry queue : queues) {
|
||||
if (vehicleId.equals(queue.getVehicleId())
|
||||
&& queue.getExecutionStatus() != 4) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private VehicleSnapshot findVehicle(
|
||||
String vehicleId,
|
||||
Collection<VehicleSnapshot> vehicles
|
||||
) {
|
||||
for (VehicleSnapshot vehicle : vehicles) {
|
||||
if (vehicleId.equals(vehicle.getVehicleId())) {
|
||||
return vehicle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requireInputs(
|
||||
VehicleSnapshot vehicle,
|
||||
DispatchQueueEntry queue,
|
||||
Collection<VehicleSnapshot> vehicles,
|
||||
Collection<DispatchQueueEntry> queues
|
||||
) {
|
||||
if (vehicle == null || queue == null || vehicles == null || queues == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"vehicle, queue, vehicles and queues are required"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.ft546.wcs.demo.traffic;
|
||||
|
||||
import com.ft546.wcs.demo.model.DecisionCode;
|
||||
import com.ft546.wcs.demo.model.DispatchQueueEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class TrafficDecision {
|
||||
private final DecisionCode code;
|
||||
private final List<String> path;
|
||||
private final DispatchQueueEntry avoidanceQueue;
|
||||
private final String reason;
|
||||
|
||||
private TrafficDecision(
|
||||
DecisionCode code,
|
||||
List<String> path,
|
||||
DispatchQueueEntry avoidanceQueue,
|
||||
String reason
|
||||
) {
|
||||
this.code = code;
|
||||
this.path = Collections.unmodifiableList(new ArrayList<String>(path));
|
||||
this.avoidanceQueue = avoidanceQueue;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public static TrafficDecision path(List<String> path) {
|
||||
return new TrafficDecision(
|
||||
DecisionCode.PATH_READY,
|
||||
path,
|
||||
null,
|
||||
"path ready: " + path
|
||||
);
|
||||
}
|
||||
|
||||
public static TrafficDecision hold(DecisionCode code, String reason) {
|
||||
return new TrafficDecision(code, Collections.<String>emptyList(), null, reason);
|
||||
}
|
||||
|
||||
public static TrafficDecision avoidance(
|
||||
DispatchQueueEntry avoidanceQueue,
|
||||
String reason
|
||||
) {
|
||||
return new TrafficDecision(
|
||||
DecisionCode.AVOIDANCE_CREATED,
|
||||
Collections.<String>emptyList(),
|
||||
avoidanceQueue,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
public DecisionCode getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public List<String> getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Optional<DispatchQueueEntry> getAvoidanceQueue() {
|
||||
return Optional.ofNullable(avoidanceQueue);
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user