#
Administrator
2026-04-25 f7629d0de2d3dd8cd7f96a1f130bbc05b644c7c1
src/main/java/com/zy/common/utils/NavigateUtils.java
@@ -6,16 +6,22 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
import com.zy.core.News;
import com.zy.core.model.StationObjModel;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.SpringUtils;
import com.core.exception.CoolException;
import com.zy.common.model.NavigateNode;
@@ -30,11 +36,27 @@
@Component
public class NavigateUtils {
    private static final long STATION_PATH_RUNTIME_SNAPSHOT_TTL_MS = 2000L;
    private static final String CFG_STATION_PATH_LEN_WEIGHT_PERCENT = "stationPathLenWeightPercent";
    private static final String CFG_STATION_PATH_CONG_WEIGHT_PERCENT = "stationPathCongWeightPercent";
    private static final String CFG_STATION_PATH_PASS_OTHER_OUT_STATION_WEIGHT_PERCENT = "stationPathPassOtherOutStationWeightPercent";
    private static final String CFG_STATION_PATH_PASS_OTHER_OUT_STATION_FORCE_SKIP = "stationPathPassOtherOutStationForceSkip";
    @Autowired
    private BasStationService basStationService;
    public synchronized List<NavigateNode> calcByStationId(Integer startStationId, Integer endStationId) {
    private final AtomicReference<CachedStationPathRuntimeSnapshot> runtimeSnapshotRef = new AtomicReference<>();
    @Scheduled(fixedDelay = 1500, initialDelay = 3000)
    public void refreshStationPathCaches() {
        try {
            runtimeSnapshotRef.set(new CachedStationPathRuntimeSnapshot(System.currentTimeMillis(), buildStationPathRuntimeSnapshot()));
        } catch (Exception ignore) {
        }
    }
    public List<NavigateNode> calcByStationId(Integer startStationId, Integer endStationId) {
        BasStation startStation = basStationService.selectById(startStationId);
        if (startStation == null) {
            throw new CoolException("未找到该 起点 对应的站点数据");
@@ -45,12 +67,12 @@
        List<List<NavigateNode>> stationMap = navigateSolution.getStationMap(lev);
        NavigateNode startNode = navigateSolution.findStationNavigateNode(stationMap, startStationId);
        if (startNode == null){
        if (startNode == null) {
            throw new CoolException("未找到该 起点 对应的节点");
        }
        NavigateNode endNode = navigateSolution.findStationNavigateNode(stationMap, endStationId);
        if (endNode == null){
        if (endNode == null) {
            throw new CoolException("未找到该 终点 对应的节点");
        }
@@ -58,7 +80,8 @@
        News.info("[WCS Debug] 站点路径开始计算,startStationId={},endStationId={}", startStationId, endStationId);
        List<List<NavigateNode>> allList = navigateSolution.allSimplePaths(stationMap, startNode, endNode, 120, 500, 300);
        if (allList.isEmpty()) {
            throw new CoolException("未找到该路径");
//            throw new CoolException("未找到该路径");
            return new ArrayList<>();
        }
        News.info("[WCS Debug] 站点路径计算完成,耗时:{}ms", System.currentTimeMillis() - startTime);
@@ -70,9 +93,12 @@
        //去重
        HashSet<Integer> set = new HashSet<>();
        List<NavigateNode> fitlerList = new ArrayList<>();
        for(NavigateNode navigateNode : list){
        for (NavigateNode navigateNode : list) {
            JSONObject valuObject = JSON.parseObject(navigateNode.getNodeValue());
            if(set.add(valuObject.getInteger("stationId"))){
            if (valuObject.containsKey("rgvCalcFlag")) {
                continue;
            }
            if (set.add(valuObject.getInteger("stationId"))) {
                fitlerList.add(navigateNode);
            }
        }
@@ -80,6 +106,20 @@
        for (int i = 0; i < fitlerList.size(); i++) {
            NavigateNode currentNode = fitlerList.get(i);
            currentNode.setIsInflectionPoint(false);
            currentNode.setIsLiftTransferPoint(false);
            try {
                JSONObject valueObject = JSON.parseObject(currentNode.getNodeValue());
                if (valueObject != null) {
                    Object isLiftTransfer = valueObject.get("isLiftTransfer");
                    if (isLiftTransfer != null) {
                        String isLiftTransferStr = isLiftTransfer.toString();
                        if ("1".equals(isLiftTransferStr) || "true".equalsIgnoreCase(isLiftTransferStr)) {
                            currentNode.setIsLiftTransferPoint(true);
                        }
                    }
                }
            } catch (Exception ignore) {}
            NavigateNode nextNode = (i + 1 < fitlerList.size()) ? fitlerList.get(i + 1) : null;
            NavigateNode prevNode = (i - 1 >= 0) ? fitlerList.get(i - 1) : null;
@@ -94,7 +134,38 @@
        return fitlerList;
    }
    public synchronized List<NavigateNode> calcByTrackSiteNo(int lev, Integer startTrackSiteNo, Integer endTrackSiteNo) {
    public List<NavigateNode> calcReachablePathByStationId(Integer startStationId, Integer endStationId) {
        BasStation startStation = basStationService.selectById(startStationId);
        if (startStation == null) {
            throw new CoolException("未找到该 起点 对应的站点数据");
        }
        Integer lev = startStation.getStationLev();
        NavigateSolution navigateSolution = new NavigateSolution();
        List<List<NavigateNode>> stationMap = navigateSolution.getStationMap(lev);
        NavigateNode startNode = navigateSolution.findStationNavigateNode(stationMap, startStationId);
        if (startNode == null) {
            throw new CoolException("未找到该 起点 对应的节点");
        }
        NavigateNode endNode = navigateSolution.findStationNavigateNode(stationMap, endStationId);
        if (endNode == null) {
            throw new CoolException("未找到该 终点 对应的节点");
        }
        long startTime = System.currentTimeMillis();
        News.info("[WCS Debug] 站点快速可达路径开始计算,startStationId={},endStationId={}", startStationId, endStationId);
        NavigateNode resNode = navigateSolution.astarSearchJava(stationMap, startNode, endNode);
        if (resNode == null) {
            return new ArrayList<>();
        }
        News.info("[WCS Debug] 站点快速可达路径计算完成,耗时:{}ms", System.currentTimeMillis() - startTime);
        return buildStationPathFromFatherChain(resNode, stationMap);
    }
    public List<NavigateNode> calcByTrackSiteNo(int lev, Integer startTrackSiteNo, Integer endTrackSiteNo) {
        NavigateSolution navigateSolution = new NavigateSolution();
        List<List<NavigateNode>> rgvTrackMap = navigateSolution.getRgvTrackMap(lev);
@@ -112,7 +183,8 @@
        News.info("[WCS Debug] RGV路径开始计算,startTrackSiteNo:{},endTrackSiteNo={}", startTrackSiteNo, endTrackSiteNo);
        NavigateNode res_node = navigateSolution.astarSearchJava(rgvTrackMap, startNode, endNode);
        if (res_node == null) {
            throw new CoolException("未找到该路径");
//            throw new CoolException("未找到该路径");
            return new ArrayList<>();
        }
        News.info("[WCS Debug] RGV路径计算完成,耗时:{}ms", System.currentTimeMillis() - startTime);
@@ -163,7 +235,7 @@
        return fitlerList;
    }
    public synchronized List<NavigateNode> findLiftStationList(int lev) {
    public List<NavigateNode> findLiftStationList(int lev) {
        NavigateSolution navigateSolution = new NavigateSolution();
        List<List<NavigateNode>> stationMap = navigateSolution.getStationMap(lev);
@@ -190,34 +262,83 @@
        return liftStationList;
    }
    public synchronized List<NavigateNode> findStationBestPath(List<List<NavigateNode>> allList) {
    private List<NavigateNode> buildStationPathFromFatherChain(NavigateNode endNode, List<List<NavigateNode>> stationMap) {
        ArrayList<NavigateNode> list = new ArrayList<>();
        HashSet<NavigateNode> visited = new HashSet<>();
        int maxSteps = stationMap.size() * stationMap.get(0).size() + 5;
        int steps = 0;
        NavigateNode fatherNode = null;
        NavigateNode currentNode = endNode;
        while (currentNode != null && visited.add(currentNode) && steps++ < maxSteps) {
            currentNode.setIsInflectionPoint(false);
            currentNode.setIsLiftTransferPoint(false);
            try {
                JSONObject valueObject = JSON.parseObject(currentNode.getNodeValue());
                if (valueObject != null) {
                    Object isLiftTransfer = valueObject.get("isLiftTransfer");
                    if (isLiftTransfer != null) {
                        String isLiftTransferStr = isLiftTransfer.toString();
                        if ("1".equals(isLiftTransferStr) || "true".equalsIgnoreCase(isLiftTransferStr)) {
                            currentNode.setIsLiftTransferPoint(true);
                        }
                    }
                }
            } catch (Exception ignore) {
            }
            HashMap<String, Object> result = searchInflectionPoint(currentNode, fatherNode, currentNode.getFather());
            if (Boolean.parseBoolean(result.get("result").toString())) {
                currentNode.setIsInflectionPoint(true);
                currentNode.setDirection(result.get("direction").toString());
            }
            list.add(currentNode);
            fatherNode = currentNode;
            currentNode = currentNode.getFather();
        }
        if (steps >= maxSteps) {
            throw new CoolException("路径回溯超出安全上限,疑似存在父链循环");
        }
        Collections.reverse(list);
        for (NavigateNode navigateNode : list) {
            navigateNode.setFather(null);
        }
        HashSet<Integer> stationIdSet = new HashSet<>();
        List<NavigateNode> filterList = new ArrayList<>();
        for (NavigateNode navigateNode : list) {
            JSONObject valueObject = JSON.parseObject(navigateNode.getNodeValue());
            if (valueObject.containsKey("rgvCalcFlag")) {
                continue;
            }
            if (stationIdSet.add(valueObject.getInteger("stationId"))) {
                filterList.add(navigateNode);
            }
        }
        return filterList;
    }
    public List<NavigateNode> findStationBestPath(List<List<NavigateNode>> allList) {
        if (allList == null || allList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<Integer, StationProtocol> statusMap = new HashMap<>();
        try {
            DeviceConfigService deviceConfigService = SpringUtils.getBean(DeviceConfigService.class);
            if (deviceConfigService != null) {
                List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                        .eq("device_type", String.valueOf(SlaveType.Devp)));
                for (DeviceConfig deviceConfig : devpList) {
                    StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo());
                    if (stationThread == null) {
                        continue;
                    }
                    Map<Integer, StationProtocol> m = stationThread.getStatusMap();
                    if (m != null && !m.isEmpty()) {
                        statusMap.putAll(m);
                    }
                }
            }
        } catch (Exception ignore) {}
        StationPathRuntimeSnapshot runtimeSnapshot = loadStationPathRuntimeSnapshot();
        Map<Integer, StationProtocol> statusMap = runtimeSnapshot.statusMap;
        Set<Integer> outStationIdSet = runtimeSnapshot.outStationIdSet;
        double lenWeightPercent = runtimeSnapshot.lenWeightPercent;
        double congWeightPercent = runtimeSnapshot.congWeightPercent;
        double passOtherOutStationWeightPercent = runtimeSnapshot.passOtherOutStationWeightPercent;
        boolean forceSkipPassOtherOutStation = runtimeSnapshot.forceSkipPassOtherOutStation;
        List<List<NavigateNode>> candidates = new ArrayList<>();
        List<Integer> lens = new ArrayList<>();
        List<Integer> tasksList = new ArrayList<>();
        List<Double> congs = new ArrayList<>();
        List<Integer> passOtherOutStationCounts = new ArrayList<>();
        int skippedByPassOtherOutStation = 0;
        for (List<NavigateNode> path : allList) {
            if (path == null || path.isEmpty()) {
@@ -247,13 +368,27 @@
                }
            }
            double cong = len <= 0 ? 0.0 : (double) tasks / (double) len;
            int passOtherOutStationCount = countPassOtherOutStations(path, outStationIdSet);
            if (forceSkipPassOtherOutStation && passOtherOutStationCount > 0) {
                skippedByPassOtherOutStation++;
                News.info("[WCS Debug] 站点路径候选已跳过,因经过其他出库站点,startStationId={},endStationId={},passOtherOutStationCount={}",
                        extractStationId(path.get(0)),
                        extractStationId(path.get(path.size() - 1)),
                        passOtherOutStationCount);
                continue;
            }
            candidates.add(path);
            lens.add(len);
            tasksList.add(tasks);
            congs.add(cong);
            passOtherOutStationCounts.add(passOtherOutStationCount);
        }
        if (candidates.isEmpty()) {
            if (forceSkipPassOtherOutStation && skippedByPassOtherOutStation > 0) {
                News.info("[WCS Debug] 所有站点路径候选均因经过其他出库站点被强制跳过");
                return new ArrayList<>();
            }
            return allList.get(0);
        }
@@ -261,59 +396,50 @@
        int maxLen = Integer.MIN_VALUE;
        double minCong = Double.MAX_VALUE;
        double maxCong = -Double.MAX_VALUE;
        int minPassOtherOutStationCount = Integer.MAX_VALUE;
        int maxPassOtherOutStationCount = Integer.MIN_VALUE;
        for (int i = 0; i < candidates.size(); i++) {
            int l = lens.get(i);
            double c = congs.get(i);
            int p = passOtherOutStationCounts.get(i);
            if (l < minLen) minLen = l;
            if (l > maxLen) maxLen = l;
            if (c < minCong) minCong = c;
            if (c > maxCong) maxCong = c;
            if (p < minPassOtherOutStationCount) minPassOtherOutStationCount = p;
            if (p > maxPassOtherOutStationCount) maxPassOtherOutStationCount = p;
        }
        //长度权重百分比
        double lenWeightPercent = 50.0;
        //拥堵权重百分比
        double congWeightPercent = 50.0;
        try {
            ConfigService configService = SpringUtils.getBean(ConfigService.class);
            if (configService != null) {
                Config cfgLen = configService.selectOne(new EntityWrapper<Config>().eq("code", "stationPathLenWeightPercent"));
                if (cfgLen != null && cfgLen.getValue() != null) {
                    String v = cfgLen.getValue().trim();
                    if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
                    try { lenWeightPercent = Double.parseDouble(v); } catch (Exception ignore) {}
                }
                Config cfgCong = configService.selectOne(new EntityWrapper<Config>().eq("code", "stationPathCongWeightPercent"));
                if (cfgCong != null && cfgCong.getValue() != null) {
                    String v = cfgCong.getValue().trim();
                    if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
                    try { congWeightPercent = Double.parseDouble(v); } catch (Exception ignore) {}
                }
            }
        } catch (Exception ignore) {}
        double weightSum = lenWeightPercent + congWeightPercent;
        double weightSum = lenWeightPercent + congWeightPercent + passOtherOutStationWeightPercent;
        double lenW = weightSum <= 0 ? 0.5 : lenWeightPercent / weightSum;
        double congW = weightSum <= 0 ? 0.5 : congWeightPercent / weightSum;
        double passOtherOutStationW = weightSum <= 0 ? 0.0 : passOtherOutStationWeightPercent / weightSum;
        List<NavigateNode> best = null;
        double bestCost = Double.MAX_VALUE;
        int bestPassOtherOutStationCount = Integer.MAX_VALUE;
        int bestTasks = Integer.MAX_VALUE;
        int bestLen = Integer.MAX_VALUE;
        for (int i = 0; i < candidates.size(); i++) {
            int l = lens.get(i);
            int t = tasksList.get(i);
            double c = congs.get(i);
            int p = passOtherOutStationCounts.get(i);
            //归一化
            double lenNorm = (maxLen - minLen) <= 0 ? 0.0 : (l - minLen) / (double) (maxLen - minLen);
            double congNorm = (maxCong - minCong) <= 0 ? 0.0 : (c - minCong) / (double) (maxCong - minCong);
            double passOtherOutStationNorm = (maxPassOtherOutStationCount - minPassOtherOutStationCount) <= 0
                    ? 0.0
                    : (p - minPassOtherOutStationCount) / (double) (maxPassOtherOutStationCount - minPassOtherOutStationCount);
            //获取权重
            double cost = lenNorm * lenW + congNorm * congW;
            double cost = lenNorm * lenW + congNorm * congW + passOtherOutStationNorm * passOtherOutStationW;
            if (cost < bestCost
                    || (cost == bestCost && t < bestTasks)
                    || (cost == bestCost && t == bestTasks && l < bestLen)) {
                    || (cost == bestCost && p < bestPassOtherOutStationCount)
                    || (cost == bestCost && p == bestPassOtherOutStationCount && t < bestTasks)
                    || (cost == bestCost && p == bestPassOtherOutStationCount && t == bestTasks && l < bestLen)) {
                best = candidates.get(i);
                bestCost = cost;
                bestPassOtherOutStationCount = p;
                bestTasks = t;
                bestLen = l;
            }
@@ -323,6 +449,155 @@
            return allList.get(0);
        }
        return best;
    }
    private Set<Integer> loadAllOutStationIdSet() {
        Set<Integer> outStationIdSet = new HashSet<>();
        try {
            BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
            if (basDevpService == null) {
                return outStationIdSet;
            }
            List<BasDevp> basDevpList = basDevpService.selectList(new EntityWrapper<BasDevp>().eq("status", 1));
            for (BasDevp basDevp : basDevpList) {
                List<StationObjModel> outStationList = basDevp.getOutStationList$();
                for (StationObjModel stationObjModel : outStationList) {
                    if (stationObjModel != null && stationObjModel.getStationId() != null) {
                        outStationIdSet.add(stationObjModel.getStationId());
                    }
                }
            }
        } catch (Exception ignore) {}
        return outStationIdSet;
    }
    private int countPassOtherOutStations(List<NavigateNode> path, Set<Integer> outStationIdSet) {
        if (path == null || path.size() < 3 || outStationIdSet == null || outStationIdSet.isEmpty()) {
            return 0;
        }
        Integer startStationId = extractStationId(path.get(0));
        Integer endStationId = extractStationId(path.get(path.size() - 1));
        Set<Integer> hitStationIdSet = new HashSet<>();
        for (int i = 1; i < path.size() - 1; i++) {
            Integer stationId = extractStationId(path.get(i));
            if (stationId == null) {
                continue;
            }
            if (startStationId != null && startStationId.equals(stationId)) {
                continue;
            }
            if (endStationId != null && endStationId.equals(stationId)) {
                continue;
            }
            if (outStationIdSet.contains(stationId)) {
                hitStationIdSet.add(stationId);
            }
        }
        return hitStationIdSet.size();
    }
    private Integer extractStationId(NavigateNode node) {
        if (node == null || node.getNodeValue() == null) {
            return null;
        }
        try {
            JSONObject value = JSON.parseObject(node.getNodeValue());
            if (value == null) {
                return null;
            }
            return value.getInteger("stationId");
        } catch (Exception ignore) {}
        return null;
    }
    private double loadDoubleConfig(ConfigService configService, String code, double defaultValue) {
        if (configService == null || code == null) {
            return defaultValue;
        }
        Config config = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
        if (config == null || config.getValue() == null) {
            return defaultValue;
        }
        String value = config.getValue().trim();
        if (value.endsWith("%")) {
            value = value.substring(0, value.length() - 1);
        }
        try {
            return Double.parseDouble(value);
        } catch (Exception ignore) {}
        return defaultValue;
    }
    private boolean loadBooleanConfig(ConfigService configService, String code, boolean defaultValue) {
        if (configService == null || code == null) {
            return defaultValue;
        }
        Config config = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
        if (config == null || config.getValue() == null) {
            return defaultValue;
        }
        String value = config.getValue().trim();
        if (value.isEmpty()) {
            return defaultValue;
        }
        return "1".equals(value)
                || "true".equalsIgnoreCase(value)
                || "yes".equalsIgnoreCase(value)
                || "y".equalsIgnoreCase(value)
                || "on".equalsIgnoreCase(value);
    }
    private StationPathRuntimeSnapshot loadStationPathRuntimeSnapshot() {
        long now = System.currentTimeMillis();
        CachedStationPathRuntimeSnapshot cached = runtimeSnapshotRef.get();
        if (cached != null && now - cached.loadTimeMs < STATION_PATH_RUNTIME_SNAPSHOT_TTL_MS && cached.snapshot != null) {
            return cached.snapshot;
        }
        StationPathRuntimeSnapshot snapshot = buildStationPathRuntimeSnapshot();
        runtimeSnapshotRef.set(new CachedStationPathRuntimeSnapshot(now, snapshot));
        return snapshot;
    }
    private StationPathRuntimeSnapshot buildStationPathRuntimeSnapshot() {
        StationPathRuntimeSnapshot snapshot = new StationPathRuntimeSnapshot();
        snapshot.statusMap = loadAllStationStatusMap();
        snapshot.outStationIdSet = loadAllOutStationIdSet();
        try {
            ConfigService configService = SpringUtils.getBean(ConfigService.class);
            if (configService != null) {
                snapshot.lenWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_LEN_WEIGHT_PERCENT, snapshot.lenWeightPercent);
                snapshot.congWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_CONG_WEIGHT_PERCENT, snapshot.congWeightPercent);
                snapshot.passOtherOutStationWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_PASS_OTHER_OUT_STATION_WEIGHT_PERCENT, snapshot.passOtherOutStationWeightPercent);
                snapshot.forceSkipPassOtherOutStation = loadBooleanConfig(configService, CFG_STATION_PATH_PASS_OTHER_OUT_STATION_FORCE_SKIP, false);
            }
        } catch (Exception ignore) {
        }
        return snapshot;
    }
    private Map<Integer, StationProtocol> loadAllStationStatusMap() {
        Map<Integer, StationProtocol> statusMap = new HashMap<>();
        try {
            DeviceConfigService deviceConfigService = SpringUtils.getBean(DeviceConfigService.class);
            if (deviceConfigService == null) {
                return statusMap;
            }
            List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                    .eq("device_type", String.valueOf(SlaveType.Devp)));
            for (DeviceConfig deviceConfig : devpList) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> m = stationThread.getStatusMap();
                if (m != null && !m.isEmpty()) {
                    statusMap.putAll(m);
                }
            }
        } catch (Exception ignore) {
        }
        return statusMap;
    }
    //判断当前节点到下一个节点是否为拐点
@@ -376,4 +651,23 @@
        return direction;
    }
    private static class StationPathRuntimeSnapshot {
        private Map<Integer, StationProtocol> statusMap = new HashMap<>();
        private Set<Integer> outStationIdSet = new HashSet<>();
        private double lenWeightPercent = 50.0;
        private double congWeightPercent = 50.0;
        private double passOtherOutStationWeightPercent = 100.0;
        private boolean forceSkipPassOtherOutStation = false;
    }
    private static class CachedStationPathRuntimeSnapshot {
        private final long loadTimeMs;
        private final StationPathRuntimeSnapshot snapshot;
        private CachedStationPathRuntimeSnapshot(long loadTimeMs, StationPathRuntimeSnapshot snapshot) {
            this.loadTimeMs = loadTimeMs;
            this.snapshot = snapshot;
        }
    }
}