package com.zy.acs.manager.core.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.zy.acs.common.utils.Utils; import com.zy.acs.framework.common.Cools; import com.zy.acs.framework.exception.CoolException; import com.zy.acs.manager.common.config.RedisProperties; import com.zy.acs.manager.common.utils.MapDataUtils; import com.zy.acs.manager.core.constant.MapDataConstant; import com.zy.acs.manager.core.domain.VehicleFootprint; import com.zy.acs.manager.core.service.astart.CodeNodeType; import com.zy.acs.manager.core.service.astart.DynamicNodeType; import com.zy.acs.manager.core.service.astart.MapDataDispatcher; import com.zy.acs.manager.core.service.astart.NavigateNode; import com.zy.acs.manager.core.service.astart.domain.DynamicNode; import com.zy.acs.manager.manager.entity.Agv; import com.zy.acs.manager.manager.entity.AgvDetail; import com.zy.acs.manager.manager.entity.AgvModel; import com.zy.acs.manager.manager.entity.Code; import com.zy.acs.manager.manager.service.AgvDetailService; import com.zy.acs.manager.manager.service.AgvModelService; import com.zy.acs.manager.manager.service.AgvService; import com.zy.acs.manager.manager.service.CodeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.DependsOn; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * Created by vincent on 8/6/2024 */ @Slf4j @Component @DependsOn("mapDataDispatcher") public class AvoidWaveCalculator { private static final ReentrantLock lock = new ReentrantLock(Boolean.TRUE); private static final int LOCK_TIMEOUT = 5; private ExecutorService singleThreadExecutor; private File pythonFile = null; @Autowired private MapDataDispatcher mapDataDispatcher; @Autowired private AgvService agvService; @Autowired private AgvDetailService agvDetailService; @Autowired private AgvModelService agvModelService; @Autowired private CodeService codeService; @Autowired private MapService mapService; @Autowired private RedisProperties redisProperties; @PostConstruct @SuppressWarnings("all") public void execute() { this.singleThreadExecutor = Executors.newSingleThreadExecutor(); this.singleThreadExecutor.execute(() -> { try { Thread.sleep(500); } catch (InterruptedException ignore) {} this.calcDynamicNodeWhenBoot(); }); } public boolean calcWaveScope(AgvModel agvModel) { Integer lev = MapDataDispatcher.MAP_DEFAULT_LEV; boolean lockAcquired = false; long startTime = System.currentTimeMillis(); try { if (!(lockAcquired = lock.tryLock(LOCK_TIMEOUT, TimeUnit.SECONDS))) { log.warn("AvoidWaveCalculator execute fail, cause can not acquire lock ..."); return false; } // return this.calcWaveScopeByPython(lev); return this.calcWaveScopeByJava(lev, agvModel); } catch (Exception e) { log.error(this.getClass().getSimpleName(), e); return false; } finally { if (lockAcquired) { lock.unlock(); } long during = System.currentTimeMillis() - startTime; if (during > 50) { log.info("滤波函数花费时间为:{}毫秒......", during); } } } private boolean calcWaveScopeByPython(Integer lev, AgvModel agvModel) throws Exception { if (null == agvModel.getDiameter() || agvModel.getDiameter() <= 0) { log.warn("There is no diameter or diameter value was wrong..."); } // python Double avoidDistance = MapDataUtils.getVehicleWaveSafeDistance(agvModel.getDiameter(), MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR); if (null == pythonFile) { pythonFile = loadPythonFile(); } ProcessBuilder processBuilder = new ProcessBuilder( "python" // 或者 "python3" 取决于系统配置 , pythonFile.getAbsolutePath() , String.valueOf(avoidDistance) , redisProperties.getHost() , redisProperties.getPassword() , String.valueOf(redisProperties.getPort()) , String.valueOf(redisProperties.getIndex()) ); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; StringBuilder builder = new StringBuilder(); while ((line = reader.readLine()) != null) { builder.append(line); } int exitCode = process.waitFor(); if (exitCode != 0) { log.error("Python script exited with error code: {}", exitCode); log.error("python error:{}", builder.toString()); return false; } reader.close(); if (builder.length() <= 0) { return false; } String result = builder.toString(); if (Cools.isEmpty(result)) { return false; } if (!"1".equals(result)) { log.error("Failed to call python"); return false; } return true; } private boolean calcWaveScopeByJava(Integer lev, AgvModel agvModel) throws Exception { String[][] codeMatrix = mapDataDispatcher.getCodeMatrix(lev); Double[][][] cdaMatrix = mapDataDispatcher.getCdaMatrix(lev); String[][] waveMatrix = mapDataDispatcher.initWaveMatrix(lev); DynamicNode[][] dynamicMatrix = mapDataDispatcher.getDynamicMatrix(lev); // 记录每台车的安全半径,顺带标记已按矩形方式绘制的地码 Map vehicleAvoidDistance = new HashMap<>(); Set preciseNodes = new HashSet<>(); // 先根据实时位置+角度绘制实际矩形占位 List agvList = agvService.list(new LambdaQueryWrapper<>()); for (Agv agv : agvList) { // 每次按调度列表刷新车辆当前地码与角度 AgvDetail agvDetail = agvDetailService.selectByAgvId(agv.getId()); if (null == agvDetail) { continue; } Long recentCodeId = agvDetail.getRecentCode(); if (null == recentCodeId) { continue; } Code centerCode = codeService.getCacheById(recentCodeId); if (null == centerCode) { continue; } AgvModel model = agvModelService.getByAgvId(agv.getId()); if (null == model) { model = agvModel; } double avoidDistance = resolveAvoidDistance(model, agvModel); vehicleAvoidDistance.put(agv.getUuid(), avoidDistance); // 将旋转矩形映射到网格,得到需要占用的节点集合 List footprintNodes = calcFootprintNodes(lev, centerCode, agvDetail, model, codeMatrix, cdaMatrix); if (Cools.isEmpty(footprintNodes)) { continue; } preciseNodes.add(centerCode.getData()); for (NavigateNode navigateNode : footprintNodes) { String waveNode = waveMatrix[navigateNode.getX()][navigateNode.getY()]; waveMatrix[navigateNode.getX()][navigateNode.getY()] = MapDataUtils.generateWaveNode(waveNode, agv.getUuid()); } } // 再用原来的圆形波纹补上剩余的路径锁定节点 for (int i = 0; i < dynamicMatrix.length; i++) { for (int j = 0; j < dynamicMatrix[i].length; j++) { DynamicNode dynamicNode = dynamicMatrix[i][j]; String vehicle = dynamicNode.getVehicle(); if (!DynamicNodeType.ACCESS.val.equals(vehicle) && !DynamicNodeType.BLOCK.val.equals(vehicle)) { String codeData = codeMatrix[i][j]; if (preciseNodes.contains(codeData)) { // 已经按真实矩形写过一次,避免重复占用 continue; } double avoidDistance = vehicleAvoidDistance.getOrDefault(vehicle, resolveAvoidDistance(null, agvModel)); if (avoidDistance <= 0) { continue; } List includeList = mapService.getWaveScopeByCode(lev, codeData, avoidDistance); for (NavigateNode navigateNode : includeList) { String waveNode = waveMatrix[navigateNode.getX()][navigateNode.getY()]; waveMatrix[navigateNode.getX()][navigateNode.getY()] = MapDataUtils.generateWaveNode(waveNode, vehicle); } } } } // 所有车辆同步完成后再统一写回 redis,减少频繁刷新 mapDataDispatcher.setWaveMatrix(lev, waveMatrix); return true; } // 不同车型可能直径不同,缺失时退回调用方传入的默认模型,最后再退回 0 private double resolveAvoidDistance(AgvModel agvModel, AgvModel fallback) { Integer diameter = null; if (null != agvModel && null != agvModel.getDiameter() && agvModel.getDiameter() > 0) { diameter = agvModel.getDiameter(); } else if (null != fallback) { diameter = fallback.getDiameter(); } return MapDataUtils.getVehicleWaveSafeDistance(diameter, MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR); } /** * 计算给定车辆在当前角度下真实覆盖的网格节点 */ private List calcFootprintNodes(Integer lev, Code centerCode, AgvDetail agvDetail, AgvModel agvModel , String[][] codeMatrix, Double[][][] cdaMatrix) { if (null == centerCode || null == agvDetail || null == agvModel) { return new ArrayList<>(); } int[] codeMatrixIdx = mapDataDispatcher.getCodeMatrixIdx(lev, centerCode.getData()); if (null == codeMatrixIdx) { return new ArrayList<>(); } Double centerX = cdaMatrix[codeMatrixIdx[0]][codeMatrixIdx[1]][0]; Double centerY = cdaMatrix[codeMatrixIdx[0]][codeMatrixIdx[1]][1]; if (centerX == null || centerY == null) { return new ArrayList<>(); } VehicleFootprint footprint = this.buildFootprint(agvModel); if (null == footprint) { return new ArrayList<>(); } double direction = Optional.ofNullable(agvDetail.getAgvAngle()).orElse(0D); // 现场角度是以正北为 0, 统一转换成弧度方便三角函数 double headingRad = Math.toRadians(90 - direction); // 旋转矩形先求出世界坐标包围盒,减少遍历 double[][] corners = this.buildRectangleCorners(centerX, centerY, footprint, headingRad); double minX = Arrays.stream(corners).mapToDouble(point -> point[0]).min().orElse(centerX); double maxX = Arrays.stream(corners).mapToDouble(point -> point[0]).max().orElse(centerX); double minY = Arrays.stream(corners).mapToDouble(point -> point[1]).min().orElse(centerY); double maxY = Arrays.stream(corners).mapToDouble(point -> point[1]).max().orElse(centerY); List nodeList = new ArrayList<>(); for (int i = 0; i < codeMatrix.length; i++) { for (int j = 0; j < codeMatrix[i].length; j++) { String codeData = codeMatrix[i][j]; if (CodeNodeType.NONE.val.equals(codeData)) { continue; } Double px = cdaMatrix[i][j][0]; Double py = cdaMatrix[i][j][1]; if (px == null || py == null) { continue; } // 先利用 AABB 做一次粗滤,再做精确的点内检测 if (px < minX || px > maxX || py < minY || py > maxY) { continue; } // 是否命中 形状矩阵范围 if (this.isInsideFootprint(px, py, centerX, centerY, footprint, headingRad)) { nodeList.add(new NavigateNode(i, j, codeData)); } } } return nodeList; } /** * 根据车型参数(含 head/tail/width)构建投影用的矩形,必要时降级使用 length */ private VehicleFootprint buildFootprint(AgvModel agvModel) { if (null == agvModel) { return null; } // double baseLength = Optional.ofNullable(agvModel.getLength()).filter(len -> len > 0).map(Double::valueOf).orElse(0D); // double defaultHalfLength = baseLength > 0 ? baseLength / 2.0 : 0; if (null == agvModel.getHeadOffset() || agvModel.getHeadOffset() <= 0) { throw new CoolException(agvModel.getName() + "未设置 车头长度。"); } if (null == agvModel.getTailOffset() || agvModel.getTailOffset() <= 0) { throw new CoolException(agvModel.getName() + "未设置 车尾长度。"); } if (null == agvModel.getWidth() || agvModel.getWidth() <= 0) { throw new CoolException(agvModel.getName() + "未设置 车身宽度。"); } double head = MapDataUtils.getVehicleWaveSafeDistance(agvModel.getHeadOffset(), MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR); double tail = MapDataUtils.getVehicleWaveSafeDistance(agvModel.getTailOffset(), MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR); double halfWidth = MapDataUtils.getVehicleWaveSafeDistance(agvModel.getWidth() / 2, MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR); // padding 直接沿四个方向等比例外扩,等价于给车身预留安全距离 // double padding = MapDataUtils.getVehicleWaveSafeDistance(agvModel.getDiameter(), MapDataConstant.MAX_DISTANCE_BETWEEN_ADJACENT_AGV_FACTOR) / 2.0; // if (padding > 0) { // head += padding; // tail += padding; // halfWidth += padding; // } // head = head <= 0 ? Math.max(tail, halfWidth) : head; // tail = tail <= 0 ? head : tail; // halfWidth = halfWidth <= 0 ? Math.max(head, tail) / 2.0 : halfWidth; // // if (head <= 0 || tail <= 0 || halfWidth <= 0) { // return null; // } return new VehicleFootprint(head, tail, halfWidth); } /** * 旋转矩形的四个角坐标,用来快速生成包围盒 */ private double[][] buildRectangleCorners(double centerX, double centerY, VehicleFootprint footprint, double headingRad) { double cos = Math.cos(headingRad); double sin = Math.sin(headingRad); double[][] localPoints = new double[][]{ {footprint.head, footprint.halfWidth}, {footprint.head, -footprint.halfWidth}, {-footprint.tail, footprint.halfWidth}, {-footprint.tail, -footprint.halfWidth} }; double[][] corners = new double[4][2]; for (int idx = 0; idx < localPoints.length; idx++) { double lx = localPoints[idx][0]; double ly = localPoints[idx][1]; double gx = centerX + lx * cos - ly * sin; double gy = centerY + lx * sin + ly * cos; corners[idx][0] = gx; corners[idx][1] = gy; } return corners; } private boolean isInsideFootprint(double px, double py, double centerX, double centerY, VehicleFootprint footprint, double headingRad) { double dx = px - centerX; double dy = py - centerY; // 把点绕回车辆本体的局部坐标,再做简单矩形判断 double localX = dx * Math.cos(headingRad) + dy * Math.sin(headingRad); double localY = -dx * Math.sin(headingRad) + dy * Math.cos(headingRad); return localX >= -footprint.tail && localX <= footprint.head && localY >= -footprint.halfWidth && localY <= footprint.halfWidth; } private File loadPythonFile() { File scriptFile = null; try { Resource resource = new ClassPathResource("agv.py"); InputStream is = resource.getInputStream(); scriptFile = File.createTempFile("agv", ".py"); scriptFile.deleteOnExit(); Files.copy(is, scriptFile.toPath(), StandardCopyOption.REPLACE_EXISTING); boolean executable = scriptFile.setExecutable(true); } catch (Exception e) { throw new RuntimeException(e); } return scriptFile; } public void calcDynamicNodeWhenBoot() { List agvList = agvService.list(new LambdaQueryWrapper<>()); DynamicNode[][] dynamicMatrix = mapDataDispatcher.getDynamicMatrix(null); for (Agv agv : agvList) { calcDynamicNodeByVehicle(agv, dynamicMatrix); } } public void calcDynamicNodeByVehicle(Agv agv, DynamicNode[][] dynamicMatrix) { if (null == dynamicMatrix) { dynamicMatrix = mapDataDispatcher.getDynamicMatrix(null); } AgvDetail agvDetail = agvDetailService.selectByAgvId(agv.getId()); if (null == agvDetail) { return; } if (agvDetail.getPos() == null) { return; } Long recentCode = agvDetail.getRecentCode(); if (null == recentCode) { return; } Code code = codeService.getById(recentCode); if (null == code) { return; } int[] codeMatrixIdx = mapDataDispatcher.getCodeMatrixIdx(null, code.getData()); DynamicNode dynamicNode = dynamicMatrix[codeMatrixIdx[0]][codeMatrixIdx[1]]; String vehicle = dynamicNode.getVehicle(); if (vehicle.equals(DynamicNodeType.ACCESS.val)) { mapDataDispatcher.modifyDynamicMatrix(null, Utils.singletonList(codeMatrixIdx), agv.getUuid()); } } @PreDestroy public void destroy() { this.singleThreadExecutor.shutdownNow(); } }