feat: simulate wcs traffic control

This commit is contained in:
zgw
2026-07-30 11:22:34 +08:00
parent 828a68aa6c
commit 75ff7ec5de
6 changed files with 591 additions and 0 deletions

View File

@@ -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");

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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"
);
}
}
}

View File

@@ -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;
}
}