- Go backend (server/)
- Frontend (web/, server/static/)
- Database and deployment files
- Scripts and docs
Co-Authored-By: 狸花猫/Claude-Qwen3.6-Plus 🐾
378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
/**
|
||
* 通过配置不同的 costFunc, distFunc, constraints 可以得到不同效果的 router
|
||
* generalRouter: 不限制搜索时的移动方向,避开障碍即可
|
||
* orthogonal: 线必须沿着竖直或水平方向(4个方向)
|
||
* octolinearRouter: 线沿着竖直、水平、对角线方向(8个方向)
|
||
*/
|
||
import { Util } from '@antv/g6-core';
|
||
import { deepMix } from '@antv/util';
|
||
import { getExpandedBBox, getExpandedBBoxPoint, getPolylinePoints, simplifyPolyline, isSegmentCrossingBBox, SortedArray } from './polyline-util';
|
||
var manhattanDist = function manhattanDist(p1, p2) {
|
||
return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
|
||
};
|
||
var eucliDist = function eucliDist(p1, p2) {
|
||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||
};
|
||
var straightPath = function straightPath(start, end) {
|
||
// console.warn('fallbackRoute: straight path');
|
||
return [start, end];
|
||
};
|
||
var simplePolyline = function simplePolyline(start, end, startNode, endNode, cfg) {
|
||
return simplifyPolyline(getPolylinePoints(start, end, startNode, endNode, cfg.offset));
|
||
};
|
||
// getPolylinePoints
|
||
var defaultCfg = {
|
||
offset: 20,
|
||
maxAllowedDirectionChange: Math.PI / 2,
|
||
maximumLoops: 2000,
|
||
gridSize: 10,
|
||
directions: [{
|
||
stepX: 1,
|
||
stepY: 0
|
||
}, {
|
||
stepX: -1,
|
||
stepY: 0
|
||
}, {
|
||
stepX: 0,
|
||
stepY: 1
|
||
}, {
|
||
stepX: 0,
|
||
stepY: -1
|
||
} // top
|
||
],
|
||
get penalties() {
|
||
return {
|
||
0: 0,
|
||
45: this.gridSize / 2,
|
||
90: this.gridSize / 2
|
||
};
|
||
},
|
||
distFunc: manhattanDist,
|
||
fallbackRoute: simplePolyline
|
||
};
|
||
export var octolinearCfg = {
|
||
maxAllowedDirectionChange: Math.PI / 4,
|
||
// 8 个方向: 上下左右 + 45度斜线方向
|
||
directions: [{
|
||
stepX: 1,
|
||
stepY: 0
|
||
}, {
|
||
stepX: 1,
|
||
stepY: 1
|
||
}, {
|
||
stepX: 0,
|
||
stepY: 1
|
||
}, {
|
||
stepX: -1,
|
||
stepY: 1
|
||
}, {
|
||
stepX: -1,
|
||
stepY: 0
|
||
}, {
|
||
stepX: -1,
|
||
stepY: -1
|
||
}, {
|
||
stepX: 0,
|
||
stepY: -1
|
||
}, {
|
||
stepX: 1,
|
||
stepY: -1
|
||
}],
|
||
distFunc: eucliDist,
|
||
fallbackRoute: straightPath
|
||
};
|
||
var pos2GridIx = function pos2GridIx(pos, gridSize) {
|
||
var gridIx = Math.round(Math.abs(pos / gridSize));
|
||
var sign = pos < 0 ? -1 : 1;
|
||
return gridIx < 0 ? 0 : sign * gridIx;
|
||
};
|
||
var getObstacleMap = function getObstacleMap(items, gridSize, offset) {
|
||
var map = {};
|
||
items.forEach(function (item) {
|
||
// create-edge 时,当边类型为 polyline 时 endNode 为 null
|
||
if (!item) return;
|
||
var bbox = getExpandedBBox(item.getBBox(), offset);
|
||
for (var x = pos2GridIx(bbox.minX, gridSize); x <= pos2GridIx(bbox.maxX, gridSize); x += 1) {
|
||
for (var y = pos2GridIx(bbox.minY, gridSize); y <= pos2GridIx(bbox.maxY, gridSize); y += 1) {
|
||
map["".concat(x, "|||").concat(y)] = true;
|
||
}
|
||
}
|
||
});
|
||
return map;
|
||
};
|
||
/**
|
||
* 方向角:计算从 p1 到 p2 的射线与水平线形成的夹角度数(顺时针从右侧0°转到该射线的角度)
|
||
* @param p1 PolyPoint
|
||
* @param p2 PolyPoint
|
||
*/
|
||
var getDirectionAngle = function getDirectionAngle(p1, p2) {
|
||
var deltaX = p2.x - p1.x;
|
||
var deltaY = p2.y - p1.y;
|
||
if (deltaX || deltaY) {
|
||
return Math.atan2(deltaY, deltaX);
|
||
}
|
||
return 0;
|
||
};
|
||
/**
|
||
* 方向角的改变,取小于180度角
|
||
* @param angle1
|
||
* @param angle2
|
||
*/
|
||
var getAngleDiff = function getAngleDiff(angle1, angle2) {
|
||
var directionChange = Math.abs(angle1 - angle2);
|
||
return directionChange > Math.PI ? 2 * Math.PI - directionChange : directionChange;
|
||
// return directionChange > 180 ? 360 - directionChange : directionChange;
|
||
};
|
||
// Path finder //
|
||
var estimateCost = function estimateCost(from, endPoints, distFunc) {
|
||
var min = Infinity;
|
||
for (var i = 0, len = endPoints.length; i < len; i++) {
|
||
var cost = distFunc(from, endPoints[i]);
|
||
if (cost < min) {
|
||
min = cost;
|
||
}
|
||
}
|
||
return min;
|
||
};
|
||
// 计算考虑 offset 后的 BBox 上的连接点
|
||
var getBoxPoints = function getBoxPoints(point,
|
||
// 被 gridSize 格式化后的位置(anchorPoint)
|
||
oriPoint,
|
||
// 未被 gridSize 格式化的位置(anchorPoint)
|
||
node,
|
||
// 原始节点,用于获取 bbox
|
||
anotherPoint,
|
||
// 另一端被 gridSize 格式化后的位置
|
||
cfg) {
|
||
var points = [];
|
||
// create-edge 生成边的过程中,endNode 为 null
|
||
if (!node) {
|
||
return [point];
|
||
}
|
||
var directions = cfg.directions,
|
||
offset = cfg.offset;
|
||
var bbox = node.getBBox();
|
||
var isInside = oriPoint.x > bbox.minX && oriPoint.x < bbox.maxX && oriPoint.y > bbox.minY && oriPoint.y < bbox.maxY;
|
||
var expandBBox = getExpandedBBox(bbox, offset);
|
||
for (var i in expandBBox) {
|
||
expandBBox[i] = pos2GridIx(expandBBox[i], cfg.gridSize);
|
||
}
|
||
if (isInside) {
|
||
// 如果 anchorPoint 在节点内部,允许第一段线穿过节点
|
||
for (var _i = 0, directions_1 = directions; _i < directions_1.length; _i++) {
|
||
var dir = directions_1[_i];
|
||
var bounds = [[{
|
||
x: expandBBox.minX,
|
||
y: expandBBox.minY
|
||
}, {
|
||
x: expandBBox.maxX,
|
||
y: expandBBox.minY
|
||
}], [{
|
||
x: expandBBox.minX,
|
||
y: expandBBox.minY
|
||
}, {
|
||
x: expandBBox.minX,
|
||
y: expandBBox.maxY
|
||
}], [{
|
||
x: expandBBox.maxX,
|
||
y: expandBBox.minY
|
||
}, {
|
||
x: expandBBox.maxX,
|
||
y: expandBBox.maxY
|
||
}], [{
|
||
x: expandBBox.minX,
|
||
y: expandBBox.maxY
|
||
}, {
|
||
x: expandBBox.maxX,
|
||
y: expandBBox.maxY
|
||
}]];
|
||
for (var i = 0; i < 4; i++) {
|
||
var boundLine = bounds[i];
|
||
var insterctP_1 = Util.getLineIntersect(point, {
|
||
x: point.x + dir.stepX * expandBBox.width,
|
||
y: point.y + dir.stepY * expandBBox.height
|
||
}, boundLine[0], boundLine[1]);
|
||
if (insterctP_1 && !isSegmentCrossingBBox(point, insterctP_1, bbox)) {
|
||
insterctP_1.id = "".concat(insterctP_1.x, "|||").concat(insterctP_1.y);
|
||
points.push(insterctP_1);
|
||
}
|
||
}
|
||
}
|
||
return points;
|
||
}
|
||
// 如果 anchorPoint 在节点上,只有一个可选方向
|
||
var insterctP = getExpandedBBoxPoint(expandBBox, point, anotherPoint);
|
||
insterctP.id = "".concat(insterctP.x, "|||").concat(insterctP.y);
|
||
return [insterctP];
|
||
};
|
||
var getDirectionChange = function getDirectionChange(current, neighbor, cameFrom, scaleStartPoint) {
|
||
var directionAngle = getDirectionAngle(current, neighbor);
|
||
var currentCameFrom = cameFrom[current.id];
|
||
if (!currentCameFrom) {
|
||
var startAngle = getDirectionAngle(scaleStartPoint, current);
|
||
return getAngleDiff(startAngle, directionAngle);
|
||
}
|
||
var prevDirectionAngle = getDirectionAngle({
|
||
x: currentCameFrom.x,
|
||
y: currentCameFrom.y
|
||
}, current);
|
||
return getAngleDiff(prevDirectionAngle, directionAngle);
|
||
};
|
||
var getControlPoints = function getControlPoints(current, cameFrom, scaleStartPoint, endPoint, startPoint, scaleEndPoint, gridSize) {
|
||
var controlPoints = [endPoint];
|
||
var pointZero = endPoint;
|
||
var currentId = current.id;
|
||
var currentX = current.x;
|
||
var currentY = current.y;
|
||
var lastPoint = {
|
||
x: currentX,
|
||
y: currentY,
|
||
id: currentId
|
||
};
|
||
if (getDirectionChange(lastPoint, scaleEndPoint, cameFrom, scaleStartPoint)) {
|
||
pointZero = {
|
||
x: scaleEndPoint.x === endPoint.x ? endPoint.x : lastPoint.x * gridSize,
|
||
y: scaleEndPoint.y === endPoint.y ? endPoint.y : lastPoint.y * gridSize
|
||
};
|
||
controlPoints.unshift(pointZero);
|
||
}
|
||
var currentCameFrom = cameFrom[currentId];
|
||
while (currentCameFrom && currentCameFrom.id !== currentId) {
|
||
var point = {
|
||
x: currentX,
|
||
y: currentY,
|
||
id: currentId
|
||
};
|
||
var prePoint = {
|
||
x: currentCameFrom.x,
|
||
y: currentCameFrom.y,
|
||
id: currentCameFrom.id
|
||
};
|
||
var directionChange = getDirectionChange(prePoint, point, cameFrom, scaleStartPoint);
|
||
if (directionChange) {
|
||
pointZero = {
|
||
x: prePoint.x === point.x ? pointZero.x : prePoint.x * gridSize,
|
||
y: prePoint.y === point.y ? pointZero.y : prePoint.y * gridSize
|
||
};
|
||
controlPoints.unshift(pointZero);
|
||
}
|
||
currentId = prePoint.id;
|
||
currentX = prePoint.x;
|
||
currentY = prePoint.y;
|
||
currentCameFrom = cameFrom[currentId];
|
||
}
|
||
// 和startNode对齐
|
||
controlPoints[0].x = currentX === scaleStartPoint.x ? startPoint.x : pointZero.x;
|
||
controlPoints[0].y = currentY === scaleStartPoint.y ? startPoint.y : pointZero.y;
|
||
controlPoints.unshift(startPoint);
|
||
return controlPoints;
|
||
};
|
||
export var pathFinder = function pathFinder(startPoint, endPoint, startNode, endNode, routerCfg) {
|
||
if (isNaN(startPoint.x) || isNaN(endPoint.x)) return [];
|
||
var cfg = deepMix(defaultCfg, routerCfg);
|
||
cfg.obstacles = cfg.obstacles || [];
|
||
var penalties = cfg.penalties,
|
||
gridSize = cfg.gridSize;
|
||
var map = getObstacleMap(cfg.obstacles.concat([startNode, endNode]), gridSize, cfg.offset);
|
||
var scaleStartPoint = {
|
||
x: pos2GridIx(startPoint.x, gridSize),
|
||
y: pos2GridIx(startPoint.y, gridSize)
|
||
};
|
||
var scaleEndPoint = {
|
||
x: pos2GridIx(endPoint.x, gridSize),
|
||
y: pos2GridIx(endPoint.y, gridSize)
|
||
};
|
||
startPoint.id = "".concat(scaleStartPoint.x, "|||").concat(scaleStartPoint.y);
|
||
endPoint.id = "".concat(scaleEndPoint.x, "|||").concat(scaleEndPoint.y);
|
||
var startPoints = getBoxPoints(scaleStartPoint, startPoint, startNode, scaleEndPoint, cfg);
|
||
var endPoints = getBoxPoints(scaleEndPoint, endPoint, endNode, scaleStartPoint, cfg);
|
||
startPoints.forEach(function (point) {
|
||
delete map[point.id];
|
||
});
|
||
endPoints.forEach(function (point) {
|
||
delete map[point.id];
|
||
});
|
||
var openSet = {};
|
||
var closedSet = {};
|
||
var cameFrom = {};
|
||
// 从起点到当前点已产生的 cost, default: Infinity
|
||
var gScore = {};
|
||
// 起点经过当前点到达终点预估的 cost, default: Infinity
|
||
var fScore = {};
|
||
var sortedOpenSet = new SortedArray();
|
||
// initialize
|
||
for (var i = 0; i < startPoints.length; i++) {
|
||
var firstStep = startPoints[i];
|
||
openSet[firstStep.id] = firstStep;
|
||
gScore[firstStep.id] = 0;
|
||
fScore[firstStep.id] = estimateCost(firstStep, endPoints, cfg.distFunc);
|
||
sortedOpenSet.add({
|
||
id: firstStep.id,
|
||
value: fScore[firstStep.id]
|
||
});
|
||
}
|
||
var remainLoops = cfg.maximumLoops;
|
||
var current, direction, neighbor, neighborCost, costFromStart, directionChange;
|
||
var curCost = Infinity;
|
||
var endPointMap = {};
|
||
endPoints.forEach(function (point) {
|
||
endPointMap["".concat(point.x, "|||").concat(point.y)] = true;
|
||
});
|
||
Object.keys(openSet).forEach(function (key) {
|
||
var id = openSet[key].id;
|
||
if (fScore[id] <= curCost) {
|
||
curCost = fScore[id];
|
||
current = openSet[id];
|
||
}
|
||
});
|
||
while (Object.keys(openSet).length > 0 && remainLoops > 0) {
|
||
var minId = sortedOpenSet.minId((remainLoops + 1) % 30 === 0);
|
||
if (minId) {
|
||
current = openSet[minId];
|
||
} else {
|
||
break;
|
||
}
|
||
// 如果 fScore 最小的点就是终点
|
||
if (endPointMap["".concat(current.x, "|||").concat(current.y)]) {
|
||
return getControlPoints(current, cameFrom, scaleStartPoint, endPoint, startPoint, scaleEndPoint, gridSize);
|
||
}
|
||
delete openSet[current.id];
|
||
sortedOpenSet.remove(current.id);
|
||
closedSet[current.id] = true;
|
||
// 获取符合条件的下一步的候选连接点
|
||
// 沿候选方向走一步
|
||
for (var i = 0; i < cfg.directions.length; i++) {
|
||
direction = cfg.directions[i];
|
||
var neighborId = "".concat(Math.round(current.x) + direction.stepX, "|||").concat(Math.round(current.y) + direction.stepY);
|
||
neighbor = {
|
||
x: current.x + direction.stepX,
|
||
y: current.y + direction.stepY,
|
||
id: neighborId
|
||
};
|
||
if (closedSet[neighborId]) continue;
|
||
directionChange = getDirectionChange(current, neighbor, cameFrom, scaleStartPoint);
|
||
if (directionChange > cfg.maxAllowedDirectionChange) continue;
|
||
if (map[neighborId]) continue; // 如果交叉则跳过
|
||
// 将候选点加入 openSet, 并计算每个候选点的 cost
|
||
if (!openSet[neighborId]) {
|
||
openSet[neighborId] = neighbor;
|
||
}
|
||
var directionPenalties = penalties[directionChange];
|
||
neighborCost = cfg.distFunc(current, neighbor) + (isNaN(directionPenalties) ? gridSize : directionPenalties);
|
||
costFromStart = gScore[current.id] + neighborCost;
|
||
var neighborGScore = gScore[neighborId];
|
||
if (neighborGScore && costFromStart >= neighborGScore) {
|
||
continue;
|
||
}
|
||
cameFrom[neighborId] = current;
|
||
gScore[neighborId] = costFromStart;
|
||
fScore[neighborId] = costFromStart + estimateCost(neighbor, endPoints, cfg.distFunc);
|
||
sortedOpenSet.add({
|
||
id: neighborId,
|
||
value: fScore[neighborId]
|
||
});
|
||
}
|
||
remainLoops -= 1;
|
||
}
|
||
return cfg.fallbackRoute(startPoint, endPoint, startNode, endNode, cfg);
|
||
}; |