| | |
| | | import com.core.common.Cools; |
| | | import com.core.common.DateUtils; |
| | | import com.core.common.SpringUtils; |
| | | import com.zy.asrs.domain.vo.StationCycleCapacityVo; |
| | | import com.zy.asrs.domain.vo.StationCycleLoopVo; |
| | | import com.zy.asrs.entity.BasDevp; |
| | | import com.zy.asrs.entity.BasStationOpt; |
| | | import com.zy.asrs.entity.DeviceConfig; |
| | | import com.zy.asrs.entity.DeviceDataLog; |
| | | import com.zy.asrs.service.BasDevpService; |
| | | import com.zy.asrs.service.BasStationOptService; |
| | | import com.zy.asrs.service.StationCycleCapacityService; |
| | | import com.zy.asrs.utils.Utils; |
| | | import com.zy.common.model.NavigateNode; |
| | | import com.zy.common.utils.NavigateUtils; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.cache.MessageQueue; |
| | | import com.zy.core.cache.OutputQueue; |
| | | import com.zy.core.enums.RedisKeyType; |
| | | import com.zy.core.enums.SlaveType; |
| | | import com.zy.core.enums.StationCommandType; |
| | | import com.zy.core.model.CommandResponse; |
| | |
| | | import com.zy.core.network.DeviceConnectPool; |
| | | import com.zy.core.network.ZyStationConnectDriver; |
| | | import com.zy.core.network.entity.ZyStationStatusEntity; |
| | | import com.zy.core.service.StationTaskLoopService; |
| | | import com.zy.core.thread.impl.v5.StationV5SegmentExecutor; |
| | | import com.zy.core.trace.StationTaskTraceRegistry; |
| | | import com.zy.core.utils.DeviceLogRedisKeyBuilder; |
| | | import lombok.Data; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | |
| | | import java.text.MessageFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.ArrayDeque; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | 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.Objects; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.Executors; |
| | | |
| | | @Data |
| | | @Slf4j |
| | | public class ZyStationV5Thread implements Runnable, com.zy.core.thread.StationThread { |
| | | |
| | | private static final int RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS = 60 * 60 * 24; |
| | | private static final int SHORT_PATH_REPEAT_AVOID_THRESHOLD = 2; |
| | | private static final int LOOP_REPEAT_TRIGGER_COUNT = 3; |
| | | private static final int LOCAL_LOOP_NEIGHBOR_HOP = 3; |
| | | |
| | | private List<StationProtocol> statusList = new ArrayList<>(); |
| | | private DeviceConfig deviceConfig; |
| | |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize) { |
| | | return getCommand(commandType, taskNo, stationId, targetStationId, palletSize, null); |
| | | } |
| | | |
| | | @Override |
| | | public StationCommand getCommand(StationCommandType commandType, |
| | | Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize, |
| | | Double pathLenFactor) { |
| | | StationCommand stationCommand = new StationCommand(); |
| | | stationCommand.setTaskNo(taskNo); |
| | | stationCommand.setStationId(stationId); |
| | |
| | | stationCommand.setCommandType(commandType); |
| | | |
| | | if (commandType == StationCommandType.MOVE && !stationId.equals(targetStationId)) { |
| | | List<NavigateNode> nodes = calcPathNavigateNodes(taskNo, stationId, targetStationId); |
| | | List<Integer> path = new ArrayList<>(); |
| | | List<Integer> liftTransferPath = new ArrayList<>(); |
| | | for (NavigateNode n : nodes) { |
| | | JSONObject v = JSONObject.parseObject(n.getNodeValue()); |
| | | if (v == null) { |
| | | continue; |
| | | } |
| | | Integer stationNo = v.getInteger("stationId"); |
| | | if (stationNo == null) { |
| | | continue; |
| | | } |
| | | path.add(stationNo); |
| | | if (Boolean.TRUE.equals(n.getIsLiftTransferPoint())) { |
| | | liftTransferPath.add(stationNo); |
| | | } |
| | | } |
| | | stationCommand.setNavigatePath(path); |
| | | stationCommand.setLiftTransferPath(liftTransferPath); |
| | | List<NavigateNode> nodes = calcPathNavigateNodes(taskNo, stationId, targetStationId, pathLenFactor); |
| | | return fillMoveCommandPath(stationCommand, nodes, taskNo, stationId, targetStationId); |
| | | } |
| | | return stationCommand; |
| | | } |
| | | |
| | | @Override |
| | | public synchronized StationCommand getRunBlockRerouteCommand(Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize) { |
| | | return getRunBlockRerouteCommand(taskNo, stationId, targetStationId, palletSize, null); |
| | | } |
| | | |
| | | @Override |
| | | public synchronized StationCommand getRunBlockRerouteCommand(Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize, |
| | | Double pathLenFactor) { |
| | | if (taskNo == null || taskNo <= 0 || stationId == null || targetStationId == null) { |
| | | return null; |
| | | } |
| | | if (Objects.equals(stationId, targetStationId)) { |
| | | return getCommand(StationCommandType.MOVE, taskNo, stationId, targetStationId, palletSize, pathLenFactor); |
| | | } |
| | | |
| | | RunBlockRerouteState rerouteState = loadRunBlockRerouteState(taskNo, stationId); |
| | | StationTaskLoopService taskLoopService = loadStationTaskLoopService(); |
| | | StationTaskLoopService.LoopEvaluation loopEvaluation = taskLoopService == null |
| | | ? new StationTaskLoopService.LoopEvaluation(taskNo, stationId, StationTaskLoopService.LoopIdentitySnapshot.empty(), 0, 0, false) |
| | | : taskLoopService.evaluateLoop(taskNo, stationId, true); |
| | | log.info("输送线堵塞重规划环线识别,taskNo={}, stationId={}, scopeType={}, localStationCount={}, sourceLoopStationCount={}", |
| | | taskNo, |
| | | stationId, |
| | | loopEvaluation.getLoopIdentity().getScopeType(), |
| | | loopEvaluation.getLoopIdentity().getLocalStationCount(), |
| | | loopEvaluation.getLoopIdentity().getSourceLoopStationCount()); |
| | | rerouteState.setTaskNo(taskNo); |
| | | rerouteState.setBlockStationId(stationId); |
| | | rerouteState.setLastTargetStationId(targetStationId); |
| | | rerouteState.setPlanCount((rerouteState.getPlanCount() == null ? 0 : rerouteState.getPlanCount()) + 1); |
| | | rerouteState.setLastPlanTime(System.currentTimeMillis()); |
| | | |
| | | List<List<NavigateNode>> candidatePathList = calcCandidatePathNavigateNodes(taskNo, stationId, targetStationId, pathLenFactor); |
| | | if (candidatePathList.isEmpty()) { |
| | | saveRunBlockRerouteState(rerouteState); |
| | | log.warn("输送线堵塞重规划失败,候选路径为空,taskNo={}, planCount={}, stationId={}, targetStationId={}", |
| | | taskNo, rerouteState.getPlanCount(), stationId, targetStationId); |
| | | return null; |
| | | } |
| | | |
| | | StationCommand rerouteCommand = selectAvailableRerouteCommand( |
| | | rerouteState, |
| | | loopEvaluation, |
| | | candidatePathList, |
| | | taskNo, |
| | | stationId, |
| | | targetStationId, |
| | | palletSize |
| | | ); |
| | | if (rerouteCommand == null) { |
| | | log.info("输送线堵塞重规划候选路线已全部试过,重置路线历史后重新开始,taskNo={}, planCount={}, stationId={}, targetStationId={}", |
| | | taskNo, rerouteState.getPlanCount(), stationId, targetStationId); |
| | | rerouteState.resetIssuedRoutes(); |
| | | rerouteCommand = selectAvailableRerouteCommand( |
| | | rerouteState, |
| | | loopEvaluation, |
| | | candidatePathList, |
| | | taskNo, |
| | | stationId, |
| | | targetStationId, |
| | | palletSize |
| | | ); |
| | | } |
| | | |
| | | if (rerouteCommand != null) { |
| | | saveRunBlockRerouteState(rerouteState); |
| | | if (taskLoopService != null) { |
| | | taskLoopService.recordLoopIssue(loopEvaluation, "RUN_BLOCK_REROUTE"); |
| | | } |
| | | log.info("输送线堵塞重规划选中候选路线,taskNo={}, planCount={}, stationId={}, targetStationId={}, route={}", |
| | | taskNo, rerouteState.getPlanCount(), stationId, targetStationId, JSON.toJSONString(rerouteCommand.getNavigatePath())); |
| | | return rerouteCommand; |
| | | } |
| | | |
| | | saveRunBlockRerouteState(rerouteState); |
| | | log.warn("输送线堵塞重规划未找到可下发路线,taskNo={}, planCount={}, stationId={}, targetStationId={}, triedRoutes={}", |
| | | taskNo, |
| | | rerouteState.getPlanCount(), |
| | | stationId, |
| | | targetStationId, |
| | | JSON.toJSONString(rerouteState.getIssuedRoutePathList())); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | return zyStationConnectDriver.readOriginCommand(address, length); |
| | | } |
| | | |
| | | private List<NavigateNode> calcPathNavigateNodes(Integer taskNo, Integer startStationId, Integer targetStationId) { |
| | | private List<NavigateNode> calcPathNavigateNodes(Integer taskNo, |
| | | Integer startStationId, |
| | | Integer targetStationId, |
| | | Double pathLenFactor) { |
| | | NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); |
| | | if (navigateUtils == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return navigateUtils.calcByStationId(startStationId, targetStationId, taskNo); |
| | | return navigateUtils.calcByStationId(startStationId, targetStationId, taskNo, pathLenFactor); |
| | | } |
| | | |
| | | private List<List<NavigateNode>> calcCandidatePathNavigateNodes(Integer taskNo, |
| | | Integer startStationId, |
| | | Integer targetStationId, |
| | | Double pathLenFactor) { |
| | | NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); |
| | | if (navigateUtils == null) { |
| | | return new ArrayList<>(); |
| | | } |
| | | return navigateUtils.calcCandidatePathByStationId(startStationId, targetStationId, taskNo, pathLenFactor); |
| | | } |
| | | |
| | | private StationCommand buildMoveCommand(Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize, |
| | | List<NavigateNode> nodes) { |
| | | StationCommand stationCommand = new StationCommand(); |
| | | stationCommand.setTaskNo(taskNo); |
| | | stationCommand.setStationId(stationId); |
| | | stationCommand.setTargetStaNo(targetStationId); |
| | | stationCommand.setPalletSize(palletSize); |
| | | stationCommand.setCommandType(StationCommandType.MOVE); |
| | | return fillMoveCommandPath(stationCommand, nodes, taskNo, stationId, targetStationId); |
| | | } |
| | | |
| | | private StationCommand fillMoveCommandPath(StationCommand stationCommand, |
| | | List<NavigateNode> nodes, |
| | | Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId) { |
| | | List<Integer> path = new ArrayList<>(); |
| | | List<Integer> liftTransferPath = new ArrayList<>(); |
| | | for (NavigateNode node : nodes) { |
| | | JSONObject valueObject; |
| | | try { |
| | | valueObject = JSONObject.parseObject(node.getNodeValue()); |
| | | } catch (Exception ignore) { |
| | | continue; |
| | | } |
| | | if (valueObject == null) { |
| | | continue; |
| | | } |
| | | Integer stationNo = valueObject.getInteger("stationId"); |
| | | if (stationNo == null) { |
| | | continue; |
| | | } |
| | | path.add(stationNo); |
| | | if (Boolean.TRUE.equals(node.getIsLiftTransferPoint())) { |
| | | liftTransferPath.add(stationNo); |
| | | } |
| | | } |
| | | if (path.isEmpty()) { |
| | | log.warn("输送线命令生成失败,路径为空,taskNo={}, stationId={}, targetStationId={}", |
| | | taskNo, stationId, targetStationId); |
| | | return null; |
| | | } |
| | | stationCommand.setNavigatePath(path); |
| | | stationCommand.setLiftTransferPath(liftTransferPath); |
| | | stationCommand.setTargetStaNo(path.get(path.size() - 1)); |
| | | return stationCommand; |
| | | } |
| | | |
| | | private StationCommand selectAvailableRerouteCommand(RunBlockRerouteState rerouteState, |
| | | StationTaskLoopService.LoopEvaluation loopEvaluation, |
| | | List<List<NavigateNode>> candidatePathList, |
| | | Integer taskNo, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Integer palletSize) { |
| | | if (rerouteState == null || candidatePathList == null || candidatePathList.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | Set<String> issuedRouteSignatureSet = rerouteState.getIssuedRouteSignatureSet(); |
| | | List<RerouteCandidateCommand> candidateCommandList = new ArrayList<>(); |
| | | for (List<NavigateNode> candidatePath : candidatePathList) { |
| | | StationCommand rerouteCommand = buildMoveCommand(taskNo, stationId, targetStationId, palletSize, candidatePath); |
| | | if (rerouteCommand == null || rerouteCommand.getNavigatePath() == null || rerouteCommand.getNavigatePath().isEmpty()) { |
| | | continue; |
| | | } |
| | | String routeSignature = buildPathSignature(rerouteCommand.getNavigatePath()); |
| | | if (Cools.isEmpty(routeSignature)) { |
| | | continue; |
| | | } |
| | | RerouteCandidateCommand candidateCommand = new RerouteCandidateCommand(); |
| | | candidateCommand.setCommand(rerouteCommand); |
| | | candidateCommand.setRouteSignature(routeSignature); |
| | | candidateCommand.setPathLength(rerouteCommand.getNavigatePath().size()); |
| | | candidateCommand.setIssuedCount(rerouteState.getRouteIssueCountMap().getOrDefault(routeSignature, 0)); |
| | | candidateCommand.setLoopFingerprint(loopEvaluation.getLoopIdentity().getLoopFingerprint()); |
| | | candidateCommand.setLoopIssuedCount(loopEvaluation.getExpectedLoopIssueCount()); |
| | | candidateCommand.setLoopTriggered(loopEvaluation.isLargeLoopTriggered()); |
| | | candidateCommand.setCurrentLoopHitCount(countCurrentLoopStationHit( |
| | | rerouteCommand.getNavigatePath(), |
| | | loopEvaluation.getLoopIdentity().getStationIdSet() |
| | | )); |
| | | candidateCommandList.add(candidateCommand); |
| | | } |
| | | if (candidateCommandList.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | List<RerouteCandidateCommand> orderedCandidateCommandList = reorderCandidateCommandsForLoopRelease(candidateCommandList); |
| | | for (RerouteCandidateCommand candidateCommand : orderedCandidateCommandList) { |
| | | if (candidateCommand == null || candidateCommand.getCommand() == null) { |
| | | continue; |
| | | } |
| | | if (issuedRouteSignatureSet.contains(candidateCommand.getRouteSignature())) { |
| | | continue; |
| | | } |
| | | |
| | | StationCommand rerouteCommand = candidateCommand.getCommand(); |
| | | issuedRouteSignatureSet.add(candidateCommand.getRouteSignature()); |
| | | rerouteState.getIssuedRoutePathList().add(new ArrayList<>(rerouteCommand.getNavigatePath())); |
| | | rerouteState.setLastSelectedRoute(new ArrayList<>(rerouteCommand.getNavigatePath())); |
| | | rerouteState.getRouteIssueCountMap().put( |
| | | candidateCommand.getRouteSignature(), |
| | | rerouteState.getRouteIssueCountMap().getOrDefault(candidateCommand.getRouteSignature(), 0) + 1 |
| | | ); |
| | | return rerouteCommand; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private List<RerouteCandidateCommand> reorderCandidateCommandsForLoopRelease(List<RerouteCandidateCommand> candidateCommandList) { |
| | | if (candidateCommandList == null || candidateCommandList.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | int shortestPathLength = Integer.MAX_VALUE; |
| | | int shortestPathLoopHitCount = Integer.MAX_VALUE; |
| | | boolean shortestPathOverused = false; |
| | | boolean currentLoopOverused = false; |
| | | boolean hasLongerCandidate = false; |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) { |
| | | continue; |
| | | } |
| | | shortestPathLength = Math.min(shortestPathLength, candidateCommand.getPathLength()); |
| | | } |
| | | if (shortestPathLength == Integer.MAX_VALUE) { |
| | | return candidateCommandList; |
| | | } |
| | | |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) { |
| | | continue; |
| | | } |
| | | if (candidateCommand.getPathLength() == shortestPathLength) { |
| | | shortestPathLoopHitCount = Math.min(shortestPathLoopHitCount, safeInt(candidateCommand.getCurrentLoopHitCount())); |
| | | } |
| | | if (candidateCommand.getPathLength() > shortestPathLength) { |
| | | hasLongerCandidate = true; |
| | | } |
| | | if (candidateCommand.getPathLength() == shortestPathLength |
| | | && candidateCommand.getIssuedCount() != null |
| | | && candidateCommand.getIssuedCount() >= SHORT_PATH_REPEAT_AVOID_THRESHOLD) { |
| | | shortestPathOverused = true; |
| | | } |
| | | if (!Cools.isEmpty(candidateCommand.getLoopFingerprint()) |
| | | && Boolean.TRUE.equals(candidateCommand.getLoopTriggered())) { |
| | | currentLoopOverused = true; |
| | | } |
| | | } |
| | | if (!shortestPathOverused && !currentLoopOverused) { |
| | | return candidateCommandList; |
| | | } |
| | | if (shortestPathLoopHitCount == Integer.MAX_VALUE) { |
| | | shortestPathLoopHitCount = 0; |
| | | } |
| | | |
| | | boolean hasLoopExitCandidate = false; |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand == null) { |
| | | continue; |
| | | } |
| | | if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) { |
| | | hasLoopExitCandidate = true; |
| | | break; |
| | | } |
| | | } |
| | | if (!hasLongerCandidate && !hasLoopExitCandidate) { |
| | | return candidateCommandList; |
| | | } |
| | | |
| | | List<RerouteCandidateCommand> reorderedList = new ArrayList<>(); |
| | | if (currentLoopOverused && hasLoopExitCandidate) { |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand == null) { |
| | | continue; |
| | | } |
| | | if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) { |
| | | appendCandidateIfAbsent(reorderedList, candidateCommand); |
| | | } |
| | | } |
| | | } |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand != null |
| | | && candidateCommand.getPathLength() != null |
| | | && candidateCommand.getPathLength() > shortestPathLength) { |
| | | appendCandidateIfAbsent(reorderedList, candidateCommand); |
| | | } |
| | | } |
| | | for (RerouteCandidateCommand candidateCommand : candidateCommandList) { |
| | | if (candidateCommand == null || candidateCommand.getPathLength() == null) { |
| | | continue; |
| | | } |
| | | appendCandidateIfAbsent(reorderedList, candidateCommand); |
| | | } |
| | | return reorderedList; |
| | | } |
| | | |
| | | private void appendCandidateIfAbsent(List<RerouteCandidateCommand> reorderedList, |
| | | RerouteCandidateCommand candidateCommand) { |
| | | if (reorderedList == null || candidateCommand == null) { |
| | | return; |
| | | } |
| | | if (!reorderedList.contains(candidateCommand)) { |
| | | reorderedList.add(candidateCommand); |
| | | } |
| | | } |
| | | |
| | | private RunBlockRerouteState loadRunBlockRerouteState(Integer taskNo, Integer blockStationId) { |
| | | if (redisUtil == null || taskNo == null || taskNo <= 0 || blockStationId == null || blockStationId <= 0) { |
| | | return new RunBlockRerouteState(); |
| | | } |
| | | Object stateObj = redisUtil.get(buildRunBlockRerouteStateKey(taskNo, blockStationId)); |
| | | if (stateObj == null) { |
| | | return new RunBlockRerouteState(); |
| | | } |
| | | try { |
| | | RunBlockRerouteState state = JSON.parseObject(String.valueOf(stateObj), RunBlockRerouteState.class); |
| | | return state == null ? new RunBlockRerouteState() : state.normalize(); |
| | | } catch (Exception ignore) { |
| | | return new RunBlockRerouteState(); |
| | | } |
| | | } |
| | | |
| | | private void saveRunBlockRerouteState(RunBlockRerouteState rerouteState) { |
| | | if (redisUtil == null |
| | | || rerouteState == null |
| | | || rerouteState.getTaskNo() == null |
| | | || rerouteState.getTaskNo() <= 0 |
| | | || rerouteState.getBlockStationId() == null |
| | | || rerouteState.getBlockStationId() <= 0) { |
| | | return; |
| | | } |
| | | rerouteState.normalize(); |
| | | redisUtil.set( |
| | | buildRunBlockRerouteStateKey(rerouteState.getTaskNo(), rerouteState.getBlockStationId()), |
| | | JSON.toJSONString(rerouteState), |
| | | RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS |
| | | ); |
| | | } |
| | | |
| | | private TaskLoopRerouteState loadTaskLoopRerouteState(Integer taskNo) { |
| | | if (redisUtil == null || taskNo == null || taskNo <= 0) { |
| | | return new TaskLoopRerouteState(); |
| | | } |
| | | Object stateObj = redisUtil.get(RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskNo); |
| | | if (stateObj == null) { |
| | | return new TaskLoopRerouteState(); |
| | | } |
| | | try { |
| | | TaskLoopRerouteState state = JSON.parseObject(String.valueOf(stateObj), TaskLoopRerouteState.class); |
| | | return state == null ? new TaskLoopRerouteState() : state.normalize(); |
| | | } catch (Exception ignore) { |
| | | return new TaskLoopRerouteState(); |
| | | } |
| | | } |
| | | |
| | | private void saveTaskLoopRerouteState(TaskLoopRerouteState taskLoopRerouteState) { |
| | | if (redisUtil == null |
| | | || taskLoopRerouteState == null |
| | | || taskLoopRerouteState.getTaskNo() == null |
| | | || taskLoopRerouteState.getTaskNo() <= 0) { |
| | | return; |
| | | } |
| | | taskLoopRerouteState.normalize(); |
| | | redisUtil.set( |
| | | RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskLoopRerouteState.getTaskNo(), |
| | | JSON.toJSONString(taskLoopRerouteState), |
| | | RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS |
| | | ); |
| | | } |
| | | |
| | | private void touchTaskLoopRerouteState(TaskLoopRerouteState taskLoopRerouteState, |
| | | LoopIdentity currentLoopIdentity) { |
| | | if (taskLoopRerouteState == null || currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) { |
| | | return; |
| | | } |
| | | taskLoopRerouteState.getLoopIssueCountMap().put( |
| | | currentLoopIdentity.getLoopFingerprint(), |
| | | taskLoopRerouteState.getLoopIssueCountMap().getOrDefault(currentLoopIdentity.getLoopFingerprint(), 0) + 1 |
| | | ); |
| | | taskLoopRerouteState.setLastLoopFingerprint(currentLoopIdentity.getLoopFingerprint()); |
| | | taskLoopRerouteState.setLastIssueTime(System.currentTimeMillis()); |
| | | } |
| | | |
| | | private int resolveCurrentLoopIssuedCount(TaskLoopRerouteState taskLoopRerouteState, |
| | | LoopIdentity currentLoopIdentity) { |
| | | if (taskLoopRerouteState == null || currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) { |
| | | return 0; |
| | | } |
| | | return taskLoopRerouteState.getLoopIssueCountMap().getOrDefault(currentLoopIdentity.getLoopFingerprint(), 0); |
| | | } |
| | | |
| | | private int resolveExpectedLoopIssuedCount(TaskLoopRerouteState taskLoopRerouteState, |
| | | LoopIdentity currentLoopIdentity) { |
| | | if (currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) { |
| | | return 0; |
| | | } |
| | | return resolveCurrentLoopIssuedCount(taskLoopRerouteState, currentLoopIdentity) + 1; |
| | | } |
| | | |
| | | private boolean isLoopRepeatTriggered(Integer loopIssuedCount) { |
| | | return loopIssuedCount != null && loopIssuedCount >= LOOP_REPEAT_TRIGGER_COUNT; |
| | | } |
| | | |
| | | private void syncTaskTraceLoopAlert(Integer taskNo, |
| | | Integer blockedStationId, |
| | | LoopIdentity currentLoopIdentity, |
| | | int loopIssuedCount) { |
| | | StationTaskTraceRegistry traceRegistry; |
| | | try { |
| | | traceRegistry = SpringUtils.getBean(StationTaskTraceRegistry.class); |
| | | } catch (Exception ignore) { |
| | | return; |
| | | } |
| | | if (traceRegistry == null || taskNo == null || taskNo <= 0) { |
| | | return; |
| | | } |
| | | |
| | | boolean active = currentLoopIdentity != null |
| | | && !Cools.isEmpty(currentLoopIdentity.getLoopFingerprint()) |
| | | && isLoopRepeatTriggered(loopIssuedCount); |
| | | Map<String, Object> details = new HashMap<>(); |
| | | details.put("blockedStationId", blockedStationId); |
| | | details.put("loopScopeType", currentLoopIdentity == null ? "" : currentLoopIdentity.getScopeType()); |
| | | details.put("loopStationCount", currentLoopIdentity == null ? 0 : currentLoopIdentity.getLocalStationCount()); |
| | | details.put("sourceLoopStationCount", currentLoopIdentity == null ? 0 : currentLoopIdentity.getSourceLoopStationCount()); |
| | | details.put("loopRepeatCount", loopIssuedCount); |
| | | String loopAlertType = resolveLoopAlertType(currentLoopIdentity); |
| | | String loopAlertText = buildLoopAlertText(loopAlertType, currentLoopIdentity, loopIssuedCount); |
| | | traceRegistry.updateLoopHint(taskNo, active, loopAlertType, loopAlertText, loopIssuedCount, details); |
| | | } |
| | | |
| | | private String resolveLoopAlertType(LoopIdentity currentLoopIdentity) { |
| | | if (currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) { |
| | | return ""; |
| | | } |
| | | return "wholeLoop".equals(currentLoopIdentity.getScopeType()) ? "LARGE_LOOP" : "SMALL_LOOP"; |
| | | } |
| | | |
| | | private String buildLoopAlertText(String loopAlertType, |
| | | LoopIdentity currentLoopIdentity, |
| | | int loopIssuedCount) { |
| | | if (Cools.isEmpty(loopAlertType) || currentLoopIdentity == null || !isLoopRepeatTriggered(loopIssuedCount)) { |
| | | return ""; |
| | | } |
| | | String typeLabel = "LARGE_LOOP".equals(loopAlertType) ? "大环线" : "小环线"; |
| | | return typeLabel + "绕圈预警,累计重规划" + loopIssuedCount + "次,当前识别范围" |
| | | + currentLoopIdentity.getLocalStationCount() + "站"; |
| | | } |
| | | |
| | | private int countCurrentLoopStationHit(List<Integer> path, Set<Integer> currentLoopStationIdSet) { |
| | | if (path == null || path.isEmpty() || currentLoopStationIdSet == null || currentLoopStationIdSet.isEmpty()) { |
| | | return 0; |
| | | } |
| | | int hitCount = 0; |
| | | for (Integer stationId : path) { |
| | | if (stationId != null && currentLoopStationIdSet.contains(stationId)) { |
| | | hitCount++; |
| | | } |
| | | } |
| | | return hitCount; |
| | | } |
| | | |
| | | private String buildPathSignature(List<Integer> path) { |
| | | if (path == null || path.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (Integer stationNo : path) { |
| | | if (stationNo == null) { |
| | | continue; |
| | | } |
| | | if (builder.length() > 0) { |
| | | builder.append("->"); |
| | | } |
| | | builder.append(stationNo); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private StationTaskLoopService loadStationTaskLoopService() { |
| | | try { |
| | | return SpringUtils.getBean(StationTaskLoopService.class); |
| | | } catch (Exception ignore) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String buildRunBlockRerouteStateKey(Integer taskNo, Integer blockStationId) { |
| | | return RedisKeyType.STATION_RUN_BLOCK_REROUTE_STATE_.key + taskNo + "_" + blockStationId; |
| | | } |
| | | |
| | | private LoopIdentity resolveStationLoopIdentity(Integer stationId) { |
| | | if (stationId == null || stationId <= 0) { |
| | | return LoopIdentity.empty(); |
| | | } |
| | | try { |
| | | StationCycleCapacityService stationCycleCapacityService = SpringUtils.getBean(StationCycleCapacityService.class); |
| | | if (stationCycleCapacityService == null) { |
| | | return LoopIdentity.empty(); |
| | | } |
| | | StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot(); |
| | | if (capacityVo == null || capacityVo.getLoopList() == null || capacityVo.getLoopList().isEmpty()) { |
| | | return LoopIdentity.empty(); |
| | | } |
| | | for (StationCycleLoopVo loopVo : capacityVo.getLoopList()) { |
| | | List<Integer> loopStationIdList = normalizeLoopStationIdList(loopVo == null ? null : loopVo.getStationIdList()); |
| | | if (loopStationIdList.isEmpty() || !loopStationIdList.contains(stationId)) { |
| | | continue; |
| | | } |
| | | Set<Integer> loopStationIdSet = new HashSet<>(loopStationIdList); |
| | | Map<Integer, Set<Integer>> stationGraph = loadUndirectedStationGraph(); |
| | | List<Integer> localCycleStationIdList = resolveLocalCycleStationIdList(stationId, loopStationIdSet, stationGraph); |
| | | if (localCycleStationIdList.size() >= 3) { |
| | | return buildLoopIdentity(localCycleStationIdList, loopStationIdList.size(), "localCycle"); |
| | | } |
| | | |
| | | List<Integer> localNeighborhoodStationIdList = resolveLocalNeighborhoodStationIdList(stationId, loopStationIdSet, stationGraph); |
| | | if (localNeighborhoodStationIdList.size() >= 3 && localNeighborhoodStationIdList.size() < loopStationIdList.size()) { |
| | | return buildLoopIdentity(localNeighborhoodStationIdList, loopStationIdList.size(), "localNeighborhood"); |
| | | } |
| | | return buildLoopIdentity(loopStationIdList, loopStationIdList.size(), "wholeLoop"); |
| | | } |
| | | } catch (Exception ignore) { |
| | | } |
| | | return LoopIdentity.empty(); |
| | | } |
| | | |
| | | private LoopIdentity buildLoopIdentity(List<Integer> stationIdList, |
| | | int sourceLoopStationCount, |
| | | String scopeType) { |
| | | List<Integer> normalizedStationIdList = normalizeLoopStationIdList(stationIdList); |
| | | if (normalizedStationIdList.isEmpty()) { |
| | | return LoopIdentity.empty(); |
| | | } |
| | | return new LoopIdentity( |
| | | buildLoopFingerprint(normalizedStationIdList), |
| | | new HashSet<>(normalizedStationIdList), |
| | | sourceLoopStationCount, |
| | | normalizedStationIdList.size(), |
| | | scopeType |
| | | ); |
| | | } |
| | | |
| | | private List<Integer> resolveLocalCycleStationIdList(Integer stationId, |
| | | Set<Integer> loopStationIdSet, |
| | | Map<Integer, Set<Integer>> stationGraph) { |
| | | if (stationId == null |
| | | || stationId <= 0 |
| | | || loopStationIdSet == null |
| | | || loopStationIdSet.isEmpty() |
| | | || stationGraph == null |
| | | || stationGraph.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | Set<Integer> localNeighborhoodStationIdSet = collectLoopNeighborhoodStationIdSet( |
| | | stationId, |
| | | loopStationIdSet, |
| | | stationGraph, |
| | | LOCAL_LOOP_NEIGHBOR_HOP |
| | | ); |
| | | if (localNeighborhoodStationIdSet.size() < 3) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | Set<Integer> directNeighborStationIdSet = filterLoopNeighborStationIdSet( |
| | | stationGraph.getOrDefault(stationId, Collections.emptySet()), |
| | | localNeighborhoodStationIdSet, |
| | | stationId |
| | | ); |
| | | if (directNeighborStationIdSet.size() < 2) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | List<Integer> bestCycleStationIdList = new ArrayList<>(); |
| | | List<Integer> neighborStationIdList = new ArrayList<>(directNeighborStationIdSet); |
| | | for (int i = 0; i < neighborStationIdList.size(); i++) { |
| | | Integer leftNeighborStationId = neighborStationIdList.get(i); |
| | | if (leftNeighborStationId == null) { |
| | | continue; |
| | | } |
| | | for (int j = i + 1; j < neighborStationIdList.size(); j++) { |
| | | Integer rightNeighborStationId = neighborStationIdList.get(j); |
| | | if (rightNeighborStationId == null) { |
| | | continue; |
| | | } |
| | | List<Integer> pathBetweenNeighbors = findShortestScopePath( |
| | | leftNeighborStationId, |
| | | rightNeighborStationId, |
| | | stationId, |
| | | localNeighborhoodStationIdSet, |
| | | stationGraph |
| | | ); |
| | | if (pathBetweenNeighbors.isEmpty()) { |
| | | continue; |
| | | } |
| | | List<Integer> cycleStationIdList = new ArrayList<>(); |
| | | cycleStationIdList.add(stationId); |
| | | cycleStationIdList.addAll(pathBetweenNeighbors); |
| | | cycleStationIdList = normalizeLoopStationIdList(cycleStationIdList); |
| | | if (cycleStationIdList.size() < 3) { |
| | | continue; |
| | | } |
| | | if (bestCycleStationIdList.isEmpty() || cycleStationIdList.size() < bestCycleStationIdList.size()) { |
| | | bestCycleStationIdList = cycleStationIdList; |
| | | } |
| | | } |
| | | } |
| | | return bestCycleStationIdList; |
| | | } |
| | | |
| | | private List<Integer> resolveLocalNeighborhoodStationIdList(Integer stationId, |
| | | Set<Integer> loopStationIdSet, |
| | | Map<Integer, Set<Integer>> stationGraph) { |
| | | return normalizeLoopStationIdList(new ArrayList<>(collectLoopNeighborhoodStationIdSet( |
| | | stationId, |
| | | loopStationIdSet, |
| | | stationGraph, |
| | | LOCAL_LOOP_NEIGHBOR_HOP |
| | | ))); |
| | | } |
| | | |
| | | private Set<Integer> collectLoopNeighborhoodStationIdSet(Integer stationId, |
| | | Set<Integer> loopStationIdSet, |
| | | Map<Integer, Set<Integer>> stationGraph, |
| | | int maxHop) { |
| | | Set<Integer> neighborhoodStationIdSet = new LinkedHashSet<>(); |
| | | if (stationId == null |
| | | || stationId <= 0 |
| | | || loopStationIdSet == null |
| | | || loopStationIdSet.isEmpty() |
| | | || !loopStationIdSet.contains(stationId) |
| | | || stationGraph == null |
| | | || stationGraph.isEmpty() |
| | | || maxHop < 0) { |
| | | return neighborhoodStationIdSet; |
| | | } |
| | | |
| | | Deque<StationHopNode> queue = new ArrayDeque<>(); |
| | | queue.offer(new StationHopNode(stationId, 0)); |
| | | neighborhoodStationIdSet.add(stationId); |
| | | while (!queue.isEmpty()) { |
| | | StationHopNode current = queue.poll(); |
| | | if (current == null || current.getHop() >= maxHop) { |
| | | continue; |
| | | } |
| | | Set<Integer> neighborStationIdSet = filterLoopNeighborStationIdSet( |
| | | stationGraph.getOrDefault(current.getStationId(), Collections.emptySet()), |
| | | loopStationIdSet, |
| | | null |
| | | ); |
| | | for (Integer neighborStationId : neighborStationIdSet) { |
| | | if (neighborStationId == null || !neighborhoodStationIdSet.add(neighborStationId)) { |
| | | continue; |
| | | } |
| | | queue.offer(new StationHopNode(neighborStationId, current.getHop() + 1)); |
| | | } |
| | | } |
| | | return neighborhoodStationIdSet; |
| | | } |
| | | |
| | | private Set<Integer> filterLoopNeighborStationIdSet(Set<Integer> candidateNeighborStationIdSet, |
| | | Set<Integer> allowedStationIdSet, |
| | | Integer excludedStationId) { |
| | | Set<Integer> result = new LinkedHashSet<>(); |
| | | if (candidateNeighborStationIdSet == null || candidateNeighborStationIdSet.isEmpty() |
| | | || allowedStationIdSet == null || allowedStationIdSet.isEmpty()) { |
| | | return result; |
| | | } |
| | | for (Integer stationId : candidateNeighborStationIdSet) { |
| | | if (stationId == null |
| | | || !allowedStationIdSet.contains(stationId) |
| | | || (excludedStationId != null && excludedStationId.equals(stationId))) { |
| | | continue; |
| | | } |
| | | result.add(stationId); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<Integer> findShortestScopePath(Integer startStationId, |
| | | Integer endStationId, |
| | | Integer excludedStationId, |
| | | Set<Integer> allowedStationIdSet, |
| | | Map<Integer, Set<Integer>> stationGraph) { |
| | | if (startStationId == null |
| | | || endStationId == null |
| | | || Objects.equals(startStationId, excludedStationId) |
| | | || Objects.equals(endStationId, excludedStationId) |
| | | || allowedStationIdSet == null |
| | | || !allowedStationIdSet.contains(startStationId) |
| | | || !allowedStationIdSet.contains(endStationId) |
| | | || stationGraph == null |
| | | || stationGraph.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | Deque<Integer> queue = new ArrayDeque<>(); |
| | | Map<Integer, Integer> parentMap = new HashMap<>(); |
| | | Set<Integer> visitedStationIdSet = new HashSet<>(); |
| | | queue.offer(startStationId); |
| | | visitedStationIdSet.add(startStationId); |
| | | while (!queue.isEmpty()) { |
| | | Integer currentStationId = queue.poll(); |
| | | if (currentStationId == null) { |
| | | continue; |
| | | } |
| | | if (Objects.equals(currentStationId, endStationId)) { |
| | | break; |
| | | } |
| | | Set<Integer> neighborStationIdSet = filterLoopNeighborStationIdSet( |
| | | stationGraph.getOrDefault(currentStationId, Collections.emptySet()), |
| | | allowedStationIdSet, |
| | | excludedStationId |
| | | ); |
| | | for (Integer neighborStationId : neighborStationIdSet) { |
| | | if (neighborStationId == null || !visitedStationIdSet.add(neighborStationId)) { |
| | | continue; |
| | | } |
| | | parentMap.put(neighborStationId, currentStationId); |
| | | queue.offer(neighborStationId); |
| | | } |
| | | } |
| | | if (!visitedStationIdSet.contains(endStationId)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | |
| | | List<Integer> pathStationIdList = new ArrayList<>(); |
| | | Integer cursorStationId = endStationId; |
| | | while (cursorStationId != null) { |
| | | pathStationIdList.add(cursorStationId); |
| | | if (Objects.equals(cursorStationId, startStationId)) { |
| | | break; |
| | | } |
| | | cursorStationId = parentMap.get(cursorStationId); |
| | | } |
| | | if (pathStationIdList.isEmpty() |
| | | || !Objects.equals(pathStationIdList.get(pathStationIdList.size() - 1), startStationId)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | Collections.reverse(pathStationIdList); |
| | | return pathStationIdList; |
| | | } |
| | | |
| | | private Map<Integer, Set<Integer>> loadUndirectedStationGraph() { |
| | | NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); |
| | | if (navigateUtils == null) { |
| | | return new HashMap<>(); |
| | | } |
| | | Map<Integer, Set<Integer>> stationGraph = navigateUtils.loadUndirectedStationGraphSnapshot(); |
| | | return stationGraph == null ? new HashMap<>() : stationGraph; |
| | | } |
| | | |
| | | private List<Integer> normalizeLoopStationIdList(List<Integer> stationIdList) { |
| | | if (stationIdList == null || stationIdList.isEmpty()) { |
| | | return new ArrayList<>(); |
| | | } |
| | | List<Integer> normalizedList = new ArrayList<>(); |
| | | Set<Integer> seenStationIdSet = new HashSet<>(); |
| | | for (Integer stationId : stationIdList) { |
| | | if (stationId == null || stationId <= 0 || !seenStationIdSet.add(stationId)) { |
| | | continue; |
| | | } |
| | | normalizedList.add(stationId); |
| | | } |
| | | Collections.sort(normalizedList); |
| | | return normalizedList; |
| | | } |
| | | |
| | | private String buildLoopFingerprint(List<Integer> stationIdList) { |
| | | if (stationIdList == null || stationIdList.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (Integer stationId : stationIdList) { |
| | | if (stationId == null) { |
| | | continue; |
| | | } |
| | | if (builder.length() > 0) { |
| | | builder.append("|"); |
| | | } |
| | | builder.append(stationId); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private int safeInt(Integer value) { |
| | | return value == null ? 0 : value; |
| | | } |
| | | |
| | | @Data |
| | | private static class RunBlockRerouteState { |
| | | private Integer taskNo; |
| | | private Integer blockStationId; |
| | | private Integer planCount = 0; |
| | | private Integer lastTargetStationId; |
| | | private Long lastPlanTime; |
| | | private List<List<Integer>> issuedRoutePathList = new ArrayList<>(); |
| | | private List<Integer> lastSelectedRoute = new ArrayList<>(); |
| | | private Set<String> issuedRouteSignatureSet = new LinkedHashSet<>(); |
| | | private Map<String, Integer> routeIssueCountMap = new HashMap<>(); |
| | | |
| | | private RunBlockRerouteState normalize() { |
| | | if (planCount == null || planCount < 0) { |
| | | planCount = 0; |
| | | } |
| | | if (issuedRoutePathList == null) { |
| | | issuedRoutePathList = new ArrayList<>(); |
| | | } |
| | | if (lastSelectedRoute == null) { |
| | | lastSelectedRoute = new ArrayList<>(); |
| | | } |
| | | if (issuedRouteSignatureSet == null) { |
| | | issuedRouteSignatureSet = new LinkedHashSet<>(); |
| | | } |
| | | if (routeIssueCountMap == null) { |
| | | routeIssueCountMap = new HashMap<>(); |
| | | } |
| | | for (List<Integer> routePath : issuedRoutePathList) { |
| | | if (routePath == null || routePath.isEmpty()) { |
| | | continue; |
| | | } |
| | | String pathSignature = buildPathSignatureText(routePath); |
| | | if (!Cools.isEmpty(pathSignature)) { |
| | | issuedRouteSignatureSet.add(pathSignature); |
| | | routeIssueCountMap.putIfAbsent(pathSignature, 1); |
| | | } |
| | | } |
| | | return this; |
| | | } |
| | | |
| | | private void resetIssuedRoutes() { |
| | | this.issuedRoutePathList = new ArrayList<>(); |
| | | this.lastSelectedRoute = new ArrayList<>(); |
| | | this.issuedRouteSignatureSet = new LinkedHashSet<>(); |
| | | } |
| | | |
| | | private static String buildPathSignatureText(List<Integer> routePath) { |
| | | if (routePath == null || routePath.isEmpty()) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(); |
| | | for (Integer stationId : routePath) { |
| | | if (stationId == null) { |
| | | continue; |
| | | } |
| | | if (builder.length() > 0) { |
| | | builder.append("->"); |
| | | } |
| | | builder.append(stationId); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | } |
| | | |
| | | @Data |
| | | private static class RerouteCandidateCommand { |
| | | private StationCommand command; |
| | | private String routeSignature; |
| | | private Integer pathLength; |
| | | private Integer issuedCount; |
| | | private String loopFingerprint; |
| | | private Integer loopIssuedCount; |
| | | private Boolean loopTriggered; |
| | | private Integer currentLoopHitCount; |
| | | } |
| | | |
| | | @Data |
| | | private static class TaskLoopRerouteState { |
| | | private Integer taskNo; |
| | | private String lastLoopFingerprint; |
| | | private Long lastIssueTime; |
| | | private Map<String, Integer> loopIssueCountMap = new HashMap<>(); |
| | | |
| | | private TaskLoopRerouteState normalize() { |
| | | if (loopIssueCountMap == null) { |
| | | loopIssueCountMap = new HashMap<>(); |
| | | } |
| | | return this; |
| | | } |
| | | } |
| | | |
| | | @Data |
| | | private static class LoopIdentity { |
| | | private String loopFingerprint; |
| | | private Set<Integer> stationIdSet = new HashSet<>(); |
| | | private int sourceLoopStationCount; |
| | | private int localStationCount; |
| | | private String scopeType; |
| | | |
| | | private LoopIdentity(String loopFingerprint, |
| | | Set<Integer> stationIdSet, |
| | | int sourceLoopStationCount, |
| | | int localStationCount, |
| | | String scopeType) { |
| | | this.loopFingerprint = loopFingerprint; |
| | | this.stationIdSet = stationIdSet == null ? new HashSet<>() : stationIdSet; |
| | | this.sourceLoopStationCount = sourceLoopStationCount; |
| | | this.localStationCount = localStationCount; |
| | | this.scopeType = scopeType == null ? "" : scopeType; |
| | | } |
| | | |
| | | private static LoopIdentity empty() { |
| | | return new LoopIdentity("", new HashSet<>(), 0, 0, "none"); |
| | | } |
| | | } |
| | | |
| | | @Data |
| | | private static class StationHopNode { |
| | | private final Integer stationId; |
| | | private final int hop; |
| | | } |
| | | } |