前言
🐧🐧在线DEMO🐧🐧
本文讨论的是如何根据起点和终点规划出一条避开大于指定坡度的最短路线,目的是研究A*算法的应用场景,加深对其的理解。
前置知识:理解A*算法,juejin.cn/post/745216…
注:实现过程中,非重点研究的算法直接使用了turfjs库计算,它是一个地理空间计算库。
实现思路
- 确认起点和终点位置。
- 根据起点和终点位置来确认出一片包含两者的矩形区域。
- 将矩形区域划分成均匀的网格。
- 计算每个网格的坡度。
- 设定一个坡度阈值,将坡度大于该阈值的网格视为障碍物。
- 基于上述结果,使用
A*算法在可通行的网格中寻找最短路径。
1.确认起点和终点
let start = [108.99746771415697, 34.00421419748617];
let end = [109.00550820307224, 33.99905943761618];
//绘制起点
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(...start),
point: {
pixelSize: 10,
color: Cesium.Color.GREEN,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 20000,
},
});
//绘制终点
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(...end),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 20000,
},
});
2.确认矩形搜索区域
为了尽可能找到可通路径,我们创建的矩形区域并非局限于能包含起点和终点的最小范围,而是将该区域适当放大几倍。这样做的目的是扩展搜索空间,从而发现可能位于最小矩形区域外部的可行路线,提高路径规划的灵活性。
如下图,红色区域为原始区域,绿色区域为放大后的区域。如果在最小区域无合适路径,我们仍有冗余的可搜索空间。
let minLng = Math.min(start[0], end[0]);
let maxLng = Math.max(start[0], end[0]);
let maxLat = Math.max(start[1], end[1]);
let minLat = Math.min(start[1], end[1]);
// 创建矩形
let poly = turf.polygon([
[
[minLng, minLat],//西南
[minLng, maxLat],//西北
[maxLng, maxLat],//东北
[maxLng, minLat],//东南
[minLng, minLat],//回到起点
],
]);
//将矩形扩大5倍(借助turf.js)
let scaledPoly = turf.transformScale(poly, 5);
viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(scaledPoly.geometry.coordinates[0].flat()),
material: Cesium.Color.GREEN,
//贴地
clampToGround: true,
width:5,
}
})
3.划分为网格
根据扩大后的矩形边界创建网格,注意此时需要指定网格的大小,网格定义的越小在后面的计算中性能开销越大。还有一种定义网格大小的方案是将网格大小动态计算,与边界大小成倍数关系,这样就能根据区域大小合理的控制计算精度。
//计算扩大后的边界
let enveloped = turf.envelope(scaledPoly).bbox;
//指定网格大小
const gridSize = 0.03;
//划分为网格
let squareGrid = turf.squareGrid(enveloped, gridSize);
squareGrid.features.forEach(item => {
viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(item.geometry.coordinates[0].flat()),
material: Cesium.Color.GREEN,
clampToGround: true,
width: 1,
}
})
})
4.计算网格坡度
在路径规划前,首先要计算出每个网格的坡度,将超出阈值坡度的网格指定为障碍物。
计算单个网格的坡度的过程:
- 沿着网格边缘插值点并计算每个点位的高程
- 找出高程差距最大的两点
- 使用三角函数计算坡度值
计算网格坡度,将超出阈值的网格指定为障碍:
// 每个网格轮廓上进行插值12个,将所有插值点存入interpolatingPoints
// ----------------------------------------------------------------↓
let interpolatingPoints = [];
const splitCount = 12;
let line = turf.lineString(squareGrid.features[0].geometry.coordinates[0]);
let lineLength = turf.length(line);
squareGrid.features.forEach((item, index) => {
let positions = item.geometry.coordinates[0];
let line = turf.lineString(positions);
//在每个网格轮廓上进行插值12个
for (let j = 1; j <= splitCount; j++) {
let along = turf.along(line, lineLength * (j / splitCount)).geometry
.coordinates;
interpolatingPoints.push(Cesium.Cartographic.fromDegrees(...along));
}
});
// ----------------------------------------------------------------↑
//为每个网格添加序号、坡角、中心点属性,并找出起点和终点的网格序号
// ----------------------------------------------------------------↓
const maxSlope = 25;//可以通过的最大坡度
let startXy = [];//起点所在的网格序号
let endXy = [];//终点所在的网格序号
Cesium.sampleTerrainMostDetailed(
viewer.scene.terrainProvider,
interpolatingPoints
).then((updatePositions) => {
//计算每列有多少个网格(为了计算每个网格在整体网格中的序号(供A*算法使用))
let columnCount = Math.floor(
turf.distance(
turf.point([enveloped[0], enveloped[1]]),
turf.point([enveloped[0], enveloped[3]])
) / gridSize
);
let count = 0;
for (let i = 0; i < updatePositions.length; i += splitCount) {
let group = updatePositions.slice(i, i + splitCount);
let sort = group.sort((a, b) => a.height - b.height);
let [minHeightPos, maxHeigthPos] = [sort.at(0), sort.at(-1)];
//获取中心点
let polygon = squareGrid.features[count];
let center = turf.centroid(polygon);
//计算斜坡角度
let line = turf.lineString([
[Cesium.Math.toDegrees(maxHeigthPos.longitude), Cesium.Math.toDegrees(maxHeigthPos.latitude)],
[Cesium.Math.toDegrees(minHeightPos.longitude), Cesium.Math.toDegrees(minHeightPos.latitude)],
]);
let edge1 = turf.length(line);
let edge2 = (maxHeigthPos.height - minHeightPos.height) / 1000;
//根据两条直角边的反正切值计算坡度
let slope = Cesium.Math.toDegrees(Math.atan(edge2 / edge1));
//计算每个网格的横纵向序号(供A*算法使用)
let x = Math.floor(count / columnCount);
let y = (x + 1) * columnCount - count - 1;
let currentGrid = squareGrid.features[count];
currentGrid.properties = {
slope: slope, //坡角
center: center.geometry.coordinates,//中心点
id: `${x}-${y}`,
x: x,//横向序号
y: y,//纵向序号
};
//计算起点和终点的二维xy坐标(turf.booleanPointInPolygon计算点是否在多边形内)
if (!startXy.length && turf.booleanPointInPolygon(turf.point(start), polygon)) {
startXy = { x, y };
}
if (!endXy.length && turf.booleanPointInPolygon(turf.point(end), polygon)) {
endXy = { x, y };
}
count += 1;//count+1开始计算下一个网格的相关属性
if (currentGrid.properties.slope > maxSlope) {
viewer.entities.add({
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(currentGrid.geometry.coordinates[0].flat()),
material: Cesium.Color.RED.withAlpha(0.5),
}
})
}
}
})
// ----------------------------------------------------------------↑
5.使用 A* 算法寻路
至此我们已具备使用A*算法的所有前置条件。
再阅读下面代码前必须先理解A*算法,否则会一头雾水:juejin.cn/post/745216…
let allList = []; //所有网格
let openList = []; //待计算的网格
squareGrid.features.forEach(({ geometry: { coordinates }, properties }) => {
let obj = {
x: properties.x,
y: properties.y,
id: properties.id,
center: properties.center,
slope: properties.slope,
h: Math.sqrt(
Math.pow(properties.x - endXy.x, 2) +
Math.pow(properties.y - endXy.y, 2)
), //当前网格和终点距为未来预期代价
g: null, //当前网格和起点距离为历史代价
f: null,
parentId: null,
coordinates: coordinates[0].map((item) => [item[0], item[1]]),
};
if (properties.slope > maxSlope) {
obj.isInCloseList = 1; //障碍物就关闭,后面不再对该网格进行计算
} else {
obj.isInOpenList = 0; //该网格为待计算网格
}
allList.push(obj);
});
let startNode = allList.find(
(item) => item.x == startXy.x && item.y == startXy.y
);
startNode.g = 0;
startNode.isInOpenList = 1;
//计算好起点的代价后将它插入到openList中
openList.push(startNode);
let endNode = {};
while (openList.length) {
//根据代价逆序排序,从openList中获取到代价最小的网格(如果有多个代价相同的点,优先选择 g 值(历史代价)较小的网格,因为这更有可能导向最短路径。)
let sortedByF = openList.sort((a, b) => a.f - b.f);
let minFNodes = sortedByF.filter((item) => item.f == sortedByF[0].f);
let nodeCurrent = minFNodes.sort((a, b) => a.g - b.g)[0];
//获取代价最小的网格周围的网格
let childUp = allList.find(
(item) => item.x == nodeCurrent.x && item.y == nodeCurrent.y - 1
);
let childRight = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y
);
let childDown = allList.find(
(item) => item.x == nodeCurrent.x && item.y == nodeCurrent.y + 1
);
let childLeft = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y
);
let childList = [childUp, childRight, childDown, childLeft];
//只有当左边和上边不全是障碍物,才能走左上的网格
if (!childUp?.isInCloseList || !childLeft?.isInCloseList) {
let childLeftUp = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y - 1
);
childList.push(childLeftUp);
}
//只有当右边和上边不全是障碍物,才能走右上的网格
if (!childUp?.isInCloseList || !childRight?.isInCloseList) {
let childRightUp = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y - 1
);
childList.push(childRightUp);
}
//只有当右边和下边不全是障碍物,才能走右下的网格
if (!childDown?.isInCloseList || !childRight?.isInCloseList) {
let childRightDown = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y + 1
);
childList.push(childRightDown);
}
//只有当左边和下边不全是障碍物,才能走左下的网格
if (!childDown?.isInCloseList || !childLeft?.isInCloseList) {
let childLeftDown = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y + 1
);
childList.push(childLeftDown);
}
//遍历周围网格
for (let i = 0; i < childList.length; i++) {
let child = childList[i];
if (!child || child.isInCloseList == 1) {
//已经关闭,后面不再计算
continue;
}
//计算当前网格到它子网格的距离
let currentToChild = Math.sqrt(
Math.pow(nodeCurrent.x - child.x, 2) +
Math.pow(nodeCurrent.y - child.y, 2)
);
if (child.isInOpenList == 0) {
//从来没有被计算过,现在计算它的代价
child.parentId = nodeCurrent.id;
//子网格的历史代价是当前网格历史代价加上当前网格到子网格的距离
child.g = nodeCurrent.g + currentToChild;
//子网格的未来期望代价是子网格到终点的距离
child.h = Math.sqrt(
Math.pow(child.x - endXy.x, 2) + Math.pow(child.y - endXy.y, 2)
);
//得出最终代价
child.f = child.g + child.h;
//设置标记,表明这个子网格已经被计算过至少一次了
child.isInOpenList = 1;
openList.push(child); //将这个子网格加入到待计算列表中
} else if (child.isInOpenList == 1) {
//这个子网格被计算过
// 将子网格的父级替换为当前网格重新计算代价
let g = nodeCurrent.g + currentToChild;
//如果更换为新父级后代价比以前小,就更新一下
if (g < child.g) {
child.g = g;
child.f = child.g + child.h;
child.parentId = nodeCurrent.id;
}
}
//找到终点了,赋值后直接跳出
if (child.x == endXy.x && child.y == endXy.y) {
endNode = child;
let roadPath = [];
//溯源出路线
let currentNode = endNode;
while (currentNode) {
roadPath.push(currentNode.center);
currentNode = allList.find(({ id }) => id == currentNode.parentId);
}
let line = turf.lineString(roadPath);
viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(roadPath.flat()),
width: 5,
clampToGround: true,
material: Cesium.Color.fromCssColorString("red"),
},
});
break;
}
}
if (endNode.id) {
break;
}
//将当前网格从待计算列表中移除并将它关闭
let index = openList.findIndex(
({ x, y }) => x == nodeCurrent.x && y == nodeCurrent.y
);
openList[index].isInCloseList = 1;
openList.splice(index, 1);
}
if (!openList.length && !endNode.id) {
alert("无路可走");
}
大功到成
总结
文章主要探讨了A*算法的应用场景。如对文中提到的功能实现有更优的想法,或是掌握其他更好的解决方案,欢迎在评论区积极分享与交流,共同促进与提升!
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./Cesium/Widgets/widgets.css">
<style>
* {
padding: 0;
margin: 0;
}
html,
body {
height: 100%;
}
#map {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
<script src="./Cesium/Cesium.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/Turf.js/6.5.0/turf.min.js"></script>
<script>
Cesium.Ion.defaultAccessToken = "xxxxx";
let viewer = new Cesium.Viewer('map', {
terrain: Cesium.Terrain.fromWorldTerrain({
requestVertexNormals: true, //Needed to visualize slope
}),
fullscreenButton: false,
baseLayerPicker: false,
timeline: true,
geocoder: false,
selectionIndicator: false,
sceneModePicker: false,
shouldAnimate: true,
infoBox: false,
homeButton: false,
navigationHelpButton: false,
animation: false,
});
viewer.camera.setView({
destination: Cesium.Cartesian3.fromRadians(1.9025916109699217, 0.5935674511615108, 3395.0586965429834),
orientation: {
heading: 4.018977641484386,
pitch: -1.2137508326515745,
roll: 0,
},
});
let handler = viewer.scene.globe.terrainProviderChanged.addEventListener(e=>{
viewer.scene.globe.terrainProviderChanged.removeEventListener(handler);
analysis();
})
function analysis() {
let start = [108.99746771415697, 34.00421419748617];
let end = [109.00611576089523, 34.00031304168087];
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(...start),
point: {
pixelSize: 10,
color: Cesium.Color.GREEN,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 20000,
},
});
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(...end),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: 20000,
},
});
return;
//将矩形范围扩大
let minLng = Math.min(start[0], end[0]);
let maxLng = Math.max(start[0], end[0]);
let maxLat = Math.max(start[1], end[1]);
let minLat = Math.min(start[1], end[1]);
// 创建矩形
let poly = turf.polygon([
[
[minLng, minLat],
[minLng, maxLat],
[maxLng, maxLat],
[maxLng, minLat],
[minLng, minLat],
],
]);
//将矩形扩大5倍(借助turf.js)
let scaledPoly = turf.transformScale(poly, 5);
//计算扩大后的边界
let enveloped = turf.envelope(scaledPoly).bbox;
//指定网格大小
const gridSize = 0.03;
//划分为网格
let squareGrid = turf.squareGrid(enveloped, gridSize);
let interpolatingPoints = [];
const splitCount = 12;
let line = turf.lineString(squareGrid.features[0].geometry.coordinates[0]);
let lineLength = turf.length(line);
squareGrid.features.forEach((item, index) => {
let positions = item.geometry.coordinates[0];
let line = turf.lineString(positions);
//在每个网格轮廓上进行插值12个
for (let j = 1; j <= splitCount; j++) {
let along = turf.along(line, lineLength * (j / splitCount)).geometry
.coordinates;
interpolatingPoints.push(Cesium.Cartographic.fromDegrees(...along));
}
});
const maxSlope = 20;//可以通过的最大坡度
let startXy = [];//起点所在的网格序号
let endXy = [];//终点所在的网格序号
Cesium.sampleTerrainMostDetailed(
viewer.scene.terrainProvider,
interpolatingPoints
).then((updatePositions) => {
//计算每列有多少个网格(为了计算每个网格在整体网格中的序号(供A*算法使用))
let columnCount = Math.floor(
turf.distance(
turf.point([enveloped[0], enveloped[1]]),
turf.point([enveloped[0], enveloped[3]])
) / gridSize
);
let count = 0;
//为每个网格添加序号、坡角、中心点属性,并找出起点和终点的网格序号
for (let i = 0; i < updatePositions.length; i += splitCount) {
let group = updatePositions.slice(i, i + splitCount);
let sort = group.sort((a, b) => a.height - b.height);
let [minHeightPos, maxHeigthPos] = [sort.at(0), sort.at(-1)];
//获取中心点
let polygon = squareGrid.features[count];
let center = turf.centroid(polygon);
//计算斜坡角度
let line = turf.lineString([
[Cesium.Math.toDegrees(maxHeigthPos.longitude), Cesium.Math.toDegrees(maxHeigthPos.latitude)],
[Cesium.Math.toDegrees(minHeightPos.longitude), Cesium.Math.toDegrees(minHeightPos.latitude)],
]);
let edge1 = turf.length(line);
let edge2 = (maxHeigthPos.height - minHeightPos.height) / 1000;
//根据两条直角边的反正切值计算坡度
let slope = Cesium.Math.toDegrees(Math.atan(edge2 / edge1));
//计算每个网格的横纵向序号(供A*算法使用)
let x = Math.floor(count / columnCount);
let y = (x + 1) * columnCount - count - 1;
let currentGrid = squareGrid.features[count];
currentGrid.properties = {
slope: slope, //坡角
center: center.geometry.coordinates,//中心点
id: `${x}-${y}`,
x: x,//横向序号
y: y,//纵向序号
};
//计算起点和终点的二维xy坐标(turf.booleanPointInPolygon计算点是否在多边形内)
if (!startXy.length && turf.booleanPointInPolygon(turf.point(start), polygon)) {
startXy = { x, y };
}
if (!endXy.length && turf.booleanPointInPolygon(turf.point(end), polygon)) {
endXy = { x, y };
}
count += 1;//count+1开始计算下一个网格的相关属性
}
let allList = []; //所有网格
let openList = []; //待计算的网格
squareGrid.features.forEach(({ geometry: { coordinates }, properties }) => {
let obj = {
x: properties.x,
y: properties.y,
id: properties.id,
center: properties.center,
slope: properties.slope,
h: Math.sqrt(
Math.pow(properties.x - endXy.x, 2) +
Math.pow(properties.y - endXy.y, 2)
), //当前网格和终点距为未来预期代价
g: null, //当前网格和起点距离为历史代价
f: null,
parentId: null,
coordinates: coordinates[0].map((item) => [item[0], item[1]]),
};
if (properties.slope > maxSlope) {
obj.isInCloseList = 1; //障碍物就关闭,后面不再对该网格进行计算
} else {
obj.isInOpenList = 0; //该网格为待计算网格
}
allList.push(obj);
});
let startNode = allList.find(
(item) => item.x == startXy.x && item.y == startXy.y
);
startNode.g = 0;
startNode.isInOpenList = 1;
//计算好起点的代价后将它插入到openList中
openList.push(startNode);
let endNode = {};
while (openList.length) {
//根据代价逆序排序,从openList中获取到代价最小的网格(如果有多个代价相同的点,优先选择 g 值(历史代价)较小的网格,因为这更有可能导向最短路径。)
let sortedByF = openList.sort((a, b) => a.f - b.f);
let minFNodes = sortedByF.filter((item) => item.f == sortedByF[0].f);
let nodeCurrent = minFNodes.sort((a, b) => a.g - b.g)[0];
//获取代价最小的网格周围的网格
let childUp = allList.find(
(item) => item.x == nodeCurrent.x && item.y == nodeCurrent.y - 1
);
let childRight = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y
);
let childDown = allList.find(
(item) => item.x == nodeCurrent.x && item.y == nodeCurrent.y + 1
);
let childLeft = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y
);
let childList = [childUp, childRight, childDown, childLeft];
//只有当左边和上边不全是障碍物,才能走左上的网格
if (!childUp?.isInCloseList || !childLeft?.isInCloseList) {
let childLeftUp = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y - 1
);
childList.push(childLeftUp);
}
//只有当右边和上边不全是障碍物,才能走右上的网格
if (!childUp?.isInCloseList || !childRight?.isInCloseList) {
let childRightUp = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y - 1
);
childList.push(childRightUp);
}
//只有当右边和下边不全是障碍物,才能走右下的网格
if (!childDown?.isInCloseList || !childRight?.isInCloseList) {
let childRightDown = allList.find(
(item) => item.x == nodeCurrent.x + 1 && item.y == nodeCurrent.y + 1
);
childList.push(childRightDown);
}
//只有当左边和下边不全是障碍物,才能走左下的网格
if (!childDown?.isInCloseList || !childLeft?.isInCloseList) {
let childLeftDown = allList.find(
(item) => item.x == nodeCurrent.x - 1 && item.y == nodeCurrent.y + 1
);
childList.push(childLeftDown);
}
//遍历周围网格
for (let i = 0; i < childList.length; i++) {
let child = childList[i];
if (!child || child.isInCloseList == 1) {
//已经关闭,后面不再计算
continue;
}
//计算当前网格到它子网格的距离
let currentToChild = Math.sqrt(
Math.pow(nodeCurrent.x - child.x, 2) +
Math.pow(nodeCurrent.y - child.y, 2)
);
if (child.isInOpenList == 0) {
//从来没有被计算过,现在计算它的代价
child.parentId = nodeCurrent.id;
//子网格的历史代价是当前网格历史代价加上当前网格到子网格的距离
child.g = nodeCurrent.g + currentToChild;
//子网格的未来期望代价是子网格到终点的距离
child.h = Math.sqrt(
Math.pow(child.x - endXy.x, 2) + Math.pow(child.y - endXy.y, 2)
);
//得出最终代价
child.f = child.g + child.h;
//设置标记,表明这个子网格已经被计算过至少一次了
child.isInOpenList = 1;
openList.push(child); //将这个子网格加入到待计算列表中
} else if (child.isInOpenList == 1) {
//这个子网格被计算过
// 将子网格的父级替换为当前网格重新计算代价
let g = nodeCurrent.g + currentToChild;
//如果更换为新父级后代价比以前小,就更新一下
if (g < child.g) {
child.g = g;
child.f = child.g + child.h;
child.parentId = nodeCurrent.id;
}
}
//找到终点了,赋值后直接跳出
if (child.x == endXy.x && child.y == endXy.y) {
endNode = child;
let roadPath = [];
//溯源出路线
let currentNode = endNode;
while (currentNode) {
roadPath.push(currentNode.center);
currentNode = allList.find(({ id }) => id == currentNode.parentId);
}
let line = turf.lineString(roadPath);
viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray(roadPath.flat()),
width: 5,
clampToGround: true,
material: Cesium.Color.fromCssColorString("red"),
},
});
break;
}
}
if (endNode.id) {
break;
}
//将当前网格从待计算列表中移除并将它关闭
let index = openList.findIndex(
({ x, y }) => x == nodeCurrent.x && y == nodeCurrent.y
);
openList[index].isInCloseList = 1;
openList.splice(index, 1);
}
if (!openList.length && !endNode.id) {
alert("无路可走");
}
})
}
</script>
</html>