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<String, Double> vehicleAvoidDistance = new HashMap<>();
|
Set<String> preciseNodes = new HashSet<>();
|
|
// 先根据实时位置+角度绘制实际矩形占位
|
List<Agv> 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<NavigateNode> 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<NavigateNode> 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<NavigateNode> 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<NavigateNode> 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<Agv> 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();
|
}
|
|
}
|