测试地图最短路径搜索(一):把所有的辅助条件都铺到地图上
测试地图最短路径搜索(四):做一个稍微完整的手绘覆盖以及路径搜索(附所有代码)
这种最短路径实现方案,目前还有很多缺点。首先是路径图片上,如果画出的路很窄,好像最终结果就找不到路了。估计原因是grid中的判断点密度(40*40)比较小,部分区域的路径,恰好没有“路过”任何一个判断点,导致路径中断。现在(20190209)我也不知道这种方法能不能最终实现。
这里我做一下测试,看看搜索出的最短路径是否符合规则,显示结果看上去有点意思。因为要在地图显示1600个圆点,显示速度会非常慢。后面附上所有代码。
基本实现步骤是这样:
1、制作一个最简单的灰度图,里面有一个矩形障碍物。
2、用切片工具的配准法,生成这个图片的切片供腾讯地图调用。
3、用切片工具——实验室,生成障碍物(或者说路径)矢量数据,数据保存在输出路径中的astar.js文件里面。
4、生成的腾讯地图页面,具备了以下功能:调用切片;显示最短路径,左键定义起点,右键定义终点;障碍物中平铺红色圆点,路径中平铺绿色圆点。
障碍物灰度图是这样的:
选择的腾讯地图区域是在博鳌,点密度40。这个点密度是可以在astar.js文件调整的,太大了显示会非常慢。下面的图可以看到,障碍物切片(就是黑色的那部分)显示正确,红色点基本与障碍物重合,显示正确。绿色点与透明区域重合,是可以作为路径的地方,也是正确的。
根据前面定的规则,路径只能经过绿点,不能经过红点。下面的图是随便搜索出的几条路径。
以上进行的测试让人觉得,用这个方案可以实现地图上最短路径搜索。
map_tx.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<title>tengxun</title>
<style type="text/css">
* {
margin: 0px;
padding: 0px;
}
body, button, input, select, textarea {
font: 12px/16px Verdana, Helvetica, Arial, sans-serif;
}
p {
width: 603px;
padding-top: 3px;
overflow: hidden;
}
.btn {
width: 100px;
height: 26px
}
#container {
min-width:603px;
min-height:767px;
}
</style>
<script charset="utf-8" src="http://map.qq.com/api/js?v=2.exp&key=你自己的key"></script>
<!--#astarjs_t#-->
<script type='text/javascript' src='astar0206.js'></script>
<!--#astarjs_b#-->
<script>
//varstar_t
var graphDiagonal = new Graph(gridDest, { diagonal: true });
var start = graphDiagonal.grid[0][0];
var end = graphDiagonal.grid[0][3];
var startLng, startLat, endLng, endLat;
var pathPoi = [], pathLineArray = [];
console.log('gridBlack length:' + gridBlack.length)
console.log('gridWhite length:' + gridWhite.length)
//varstar_b
var map;
function init() {
var earthlayer = new qq.maps.ImageMapType({
name: 'tentxun',
alt: 'tentxun',
tileSize: new qq.maps.Size(256, 256),
minZoom: 14,
maxZoom: 16,
opacity: 1,
getTileUrl: function (tile, zoom) {
var z = zoom,
x = tile.x,
y = tile.y;
return 'tiles/' + z + '/' + x + '_' + y + '.png';
}
});
map = new qq.maps.Map(document.getElementById("container"), {
center: new qq.maps.LatLng(19.13446, 110.56323),
zoom: 15
});
map.overlayMapTypes.push(earthlayer);
// 测试 圆点 -------------------
for (var i=0; i<gridBlack.length ; i++)
{
for (var j=0; j<gridBlack[i].length ; j++)
{
//console.log(gridBlack[i][j][0] + '---' + gridBlack[i][j][1]);
//console.log('gridBlack['+i+']:' + gridBlack[i].length);
var center=new qq.maps.LatLng(gridBlack[i][j][1], gridBlack[i][j][0]);
var circle=new qq.maps.Circle({
map:map,
center:center,
radius:12,
fillColor:"#F00",
strokeWeight:0
});
}
}
for (var i=0; i<gridWhite.length ; i++)
{
//console.log('gridWhite['+i+']:' + gridWhite[i].length);
for (var j=0; j<gridWhite[i].length ; j++)
{
//console.log(gridWhite[i][j][0] + '---' + gridWhite[i][j][1]);
var center=new qq.maps.LatLng(gridWhite[i][j][1], gridWhite[i][j][0]);
var circle=new qq.maps.Circle({
map:map,
center:center,
radius:12,
fillColor:"#0F0",
strokeWeight:0
});
}
}
// 测试 圆点 ====================
//click_t
//添加监听事件
qq.maps.event.addListener(map, 'click', function(event) {
var latLng = event.latLng;
var lng = latLng.getLng();
var lat = latLng.getLat();
var a = lngToGC(lng, zoomStar);
var b = latToGR(lat, zoomStar);
startLng = lng;
startLat = lat;
start = graphDiagonal.grid[a][b];
drawLine();
});
qq.maps.event.addListener(map, 'rightclick', function(event) {
var latLng = event.latLng;
var lng = latLng.getLng();
var lat = latLng.getLat();
var a = lngToGC(lng, zoomStar);
var b = latToGR(lat, zoomStar);
endLng = lng;
endLat = lat;
end = graphDiagonal.grid[a][b];
drawLine();
});
//click_b
}
//drawline_t
function drawLine(){
var sTime = performance ? performance.now() : new Date().getTime();
pathlnla = astar.search(graphDiagonal, start, end, { heuristic: astar.heuristics.diagonal });
var fTime = performance ? performance.now() : new Date().getTime(),
duration = (fTime-sTime).toFixed(2);
console.log('time:' + duration);
pathPoi = [];
for (var i=0; i<pathlnla.length ; i++)
{
var s1 = pathlnla[i];
var regx = /(?<=\[)\d+\.\d+/;
var regy = /\d+\.\d+(?=\])/;
var pathPoiX = regx.exec(s1);
var pathPoiY = regy.exec(s1);
var poi = new qq.maps.LatLng(pathPoiY, pathPoiX);
pathPoi.push(poi);
}
if (pathLineArray) {
for (i in pathLineArray) {
pathLineArray[i].setMap(null);
}
}
var polyline = new qq.maps.Polyline({
path: pathPoi,
strokeColor: '#FF0000',
strokeWeight: 5,
editable:false,
map: map
});
pathLineArray.push(polyline);
}
//drawline_b
</script>
</head>
<body onload="init();">
<div id="container"></div>
<span id="info"></span>
</body>
</html>
astar0206.js
// javascript-astar 0.4.1
// http://github.com/bgrins/javascript-astar
// Freely distributable under the MIT License.
// Implements the astar search algorithm in javascript using a Binary Heap.
// Includes Binary Heap (with modifications) from Marijn Haverbeke.
// http://eloquentjavascript.net/appendix2.html
(function(definition) {
/* global module, define */
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = definition();
} else if (typeof define === 'function' && define.amd) {
define([], definition);
} else {
var exports = definition();
window.astar = exports.astar;
window.Graph = exports.Graph;
window.gridDest = exports.gridDest;
window.lngToGC = exports.lngToGC;
window.latToGR = exports.latToGR;
window.zoomStar = exports.zoomStar;
window.gridBlack = exports.gridBlack;
window.gridWhite = exports.gridWhite;
//window.startR = exports.startR;
//window.startC = exports.startC;
}
})(function() {
function pathTo(node) {
var curr = node;
var path = [];
var pathLngLat = []; // cbq
while (curr.parent) {
path.unshift(curr);
curr = curr.parent;
}
console.log('path.length:' + path.length);
var pathLngLat = [];
for (var i = 0; i < path.length; i++) {
var s1 = path[i];
var regx = /(?<=\[)\d+/;
var regy = /\d+(?=\])/;
var pathPoiX = regx.exec(s1);
var pathPoiY = regy.exec(s1);
var lng = pixToLng(deltX + cellWid * (pathPoiX), zoomStar); // polyPoiF[i].x + deltaLenX
var lat = pixToLat(deltY + cellWid * (pathPoiY), zoomStar); // polyPoiF[i].y + deltaLenY
pathLngLat[i] = '[' + lng + ',' + lat + ']';
}
return pathLngLat;
}
function getHeap() {
return new BinaryHeap(function(node) {
return node.f;
});
}
//---------------------------------------------------------------------------------
var
deltX = 13540482,
deltY = 7478025,
zoomStar = 16,
w = 2587,
c = 40;
var
cellWid = w/c,
cellPosX, cellPosY, //, startR, startC;
gridBlack = [], gridWhite = [];
var poly0 = [[816,1867.703125],[818,431],[1132,431],[1130,1869],[817.296875,1869],];
function inside(point, vs) {
var x = point[0], y = point[1];
var res = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0], yi = vs[i][1];
var xj = vs[j][0], yj = vs[j][1];
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) res = !res;
}
return res;
};
function pixToLng(pixelX, zoom){
return pixelX * 360 / (256 * Math.pow(2, zoom)) - 180;
}
function lngToGC(lng, zoom){
var f = (lng + 180) * (256 * Math.pow(2, zoom)) / 360;
var r = (f - deltX + cellWid / 2) / cellWid;
if (r > c - 1)
{
r = c - 1;
}else if(r < 0)
{
r = 0;
}
return Math.floor(r);
}
function pixToLat(pixelY, zoom){
var siny, y, z, E;
E = 2.71828182845904523536;
y = 2 * Math.PI * (1 - pixelY / (128 * Math.pow(2, zoom)));
z = Math.pow(E, y);
siny = (z - 1) / (z + 1);
return Math.asin(siny) * 180 / Math.PI;
}
function latToGR(lat, zoom){
var siny = Math.sin(lat * Math.PI / 180);
var y = Math.log((1 + siny) / (1 - siny));
var f = (128 * Math.pow(2, zoom)) * (1 - y / (2 * Math.PI));
var r = (f - deltY + cellWid / 2) / cellWid;
if (r > c - 1)
{
r = c - 1;
}else if(r < 0)
{
r = 0;
}
return Math.floor(r);
}
var gridDest = [];
for (var y = 0; y < c; y++ )
{
var gridRow = [];
var gridRowBlack = [];
var gridRowWhite = [];
for (var x = 0; x < c; x++ )
{
var isWall = 1;
var a1 = [];
cellPosX = cellWid * x;
cellPosY = cellWid * y;
//if((inside([cellPosX, cellPosY], poly0)) || (inside([cellPosX, cellPosY], poly1)) || (inside([cellPosX, cellPosY], poly2)) || (inside([cellPosX, cellPosY], poly3)) || (inside([cellPosX, cellPosY], poly4)) || (inside([cellPosX, cellPosY], poly5)) || (inside([cellPosX, cellPosY], poly6)) || (inside([cellPosX, cellPosY], poly7)) || (inside([cellPosX, cellPosY], poly8))){
if((inside([cellPosX, cellPosY], poly0))){
isWall = 0;
var lng = pixToLng(deltX + cellWid * (y), zoomStar); // polyPoiF[i].x + deltaLenX
var lat = pixToLat(deltY + cellWid * (x), zoomStar); // polyPoiF[i].y + deltaLenY
a1.push(lng);
a1.push(lat);
gridRowBlack.push(a1);
}else{
isWall = 1;
var lng = pixToLng(deltX + cellWid * (y), zoomStar); // polyPoiF[i].x + deltaLenX
var lat = pixToLat(deltY + cellWid * (x), zoomStar); // polyPoiF[i].y + deltaLenY
a1.push(lng);
a1.push(lat);
//gridRowWhite.push('[' + lng + '],[' + lat + ']');
gridRowWhite.push(a1);
}
gridRow.push(isWall);
}
gridDest.push(gridRow);
gridBlack.push(gridRowBlack);
gridWhite.push(gridRowWhite);
}
//=====================================================================================
var astar = {
/**
* Perform an A* Search on a graph given a start and end node.
* @param {Graph} graph
* @param {GridNode} start
* @param {GridNode} end
* @param {Object} [options]
* @param {bool} [options.closest] Specifies whether to return the
path to the closest node if the target is unreachable.
* @param {Function} [options.heuristic] Heuristic function (see
* astar.heuristics).
*/
search: function(graph, start, end, options) {
graph.cleanDirty();
options = options || {};
var heuristic = options.heuristic || astar.heuristics.manhattan;
var closest = options.closest || false;
var openHeap = getHeap();
var closestNode = start; // set the start node to be the closest if required
start.h = heuristic(start, end);
graph.markDirty(start);
openHeap.push(start);
while (openHeap.size() > 0) {
// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
var currentNode = openHeap.pop();
// End case -- result has been found, return the traced path.
if (currentNode === end) {
return pathTo(currentNode);
}
// Normal case -- move currentNode from open to closed, process each of its neighbors.
currentNode.closed = true;
// Find all neighbors for the current node.
var neighbors = graph.neighbors(currentNode);
for (var i = 0, il = neighbors.length; i < il; ++i) {
var neighbor = neighbors[i];
if (neighbor.closed || neighbor.isWall()) {
// Not a valid node to process, skip to next neighbor.
continue;
}
// The g score is the shortest distance from start to current node.
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
var gScore = currentNode.g + neighbor.getCost(currentNode);
var beenVisited = neighbor.visited;
if (!beenVisited || gScore < neighbor.g) {
// Found an optimal (so far) path to this node. Take score for node to see how good it is.
neighbor.visited = true;
neighbor.parent = currentNode;
neighbor.h = neighbor.h || heuristic(neighbor, end);
neighbor.g = gScore;
neighbor.f = neighbor.g + neighbor.h;
graph.markDirty(neighbor);
if (closest) {
// If the neighbour is closer than the current closestNode or if it's equally close but has
// a cheaper path than the current closest node then it becomes the closest node
if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {
closestNode = neighbor;
}
}
if (!beenVisited) {
// Pushing to heap will put it in proper place based on the 'f' value.
openHeap.push(neighbor);
} else {
// Already seen the node, but since it has been rescored we need to reorder it in the heap
openHeap.rescoreElement(neighbor);
}
}
}
}
if (closest) {
return pathTo(closestNode);
}
// No result was found - empty array signifies failure to find path.
return [];
},
// See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
heuristics: {
manhattan: function(pos0, pos1) {
var d1 = Math.abs(pos1.x - pos0.x);
var d2 = Math.abs(pos1.y - pos0.y);
return d1 + d2;
},
diagonal: function(pos0, pos1) {
var D = 1;
var D2 = Math.sqrt(2);
var d1 = Math.abs(pos1.x - pos0.x);
var d2 = Math.abs(pos1.y - pos0.y);
return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
}
},
cleanNode: function(node) {
node.f = 0;
node.g = 0;
node.h = 0;
node.visited = false;
node.closed = false;
node.parent = null;
}
};
/**
* A graph memory structure
* @param {Array} gridIn 2D array of input weights
* @param {Object} [options]
* @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
*/
function Graph(gridIn, options) {
options = options || {};
this.nodes = [];
this.diagonal = !!options.diagonal;
this.grid = [];
for (var x = 0; x < gridIn.length; x++) {
this.grid[x] = [];
for (var y = 0, row = gridIn[x]; y < row.length; y++) {
var node = new GridNode(x, y, row[y]);
this.grid[x][y] = node;
this.nodes.push(node);
}
}
this.init();
}
Graph.prototype.init = function() {
this.dirtyNodes = [];
for (var i = 0; i < this.nodes.length; i++) {
astar.cleanNode(this.nodes[i]);
}
};
Graph.prototype.cleanDirty = function() {
for (var i = 0; i < this.dirtyNodes.length; i++) {
astar.cleanNode(this.dirtyNodes[i]);
}
this.dirtyNodes = [];
};
Graph.prototype.markDirty = function(node) {
this.dirtyNodes.push(node);
};
Graph.prototype.neighbors = function(node) {
var ret = [];
var x = node.x;
var y = node.y;
var grid = this.grid;
// West
if (grid[x - 1] && grid[x - 1][y]) {
ret.push(grid[x - 1][y]);
}
// East
if (grid[x + 1] && grid[x + 1][y]) {
ret.push(grid[x + 1][y]);
}
// South
if (grid[x] && grid[x][y - 1]) {
ret.push(grid[x][y - 1]);
}
// North
if (grid[x] && grid[x][y + 1]) {
ret.push(grid[x][y + 1]);
}
if (this.diagonal) {
// Southwest
if (grid[x - 1] && grid[x - 1][y - 1]) {
ret.push(grid[x - 1][y - 1]);
}
// Southeast
if (grid[x + 1] && grid[x + 1][y - 1]) {
ret.push(grid[x + 1][y - 1]);
}
// Northwest
if (grid[x - 1] && grid[x - 1][y + 1]) {
ret.push(grid[x - 1][y + 1]);
}
// Northeast
if (grid[x + 1] && grid[x + 1][y + 1]) {
ret.push(grid[x + 1][y + 1]);
}
}
return ret;
};
Graph.prototype.toString = function() {
var graphString = [];
var nodes = this.grid;
for (var x = 0; x < nodes.length; x++) {
var rowDebug = [];
var row = nodes[x];
for (var y = 0; y < row.length; y++) {
rowDebug.push(row[y].weight);
}
graphString.push(rowDebug.join(" "));
}
return graphString.join("\n");
};
function GridNode(x, y, weight) {
this.x = x;
this.y = y;
this.weight = weight;
}
GridNode.prototype.toString = function() {
return "[" + this.x + " " + this.y + "]";
};
GridNode.prototype.getCost = function(fromNeighbor) {
// Take diagonal weight into consideration.
if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) {
return this.weight * 1.41421;
}
return this.weight;
};
GridNode.prototype.isWall = function() {
return this.weight === 0;
};
function BinaryHeap(scoreFunction) {
this.content = [];
this.scoreFunction = scoreFunction;
}
BinaryHeap.prototype = {
push: function(element) {
// Add the new element to the end of the array.
this.content.push(element);
// Allow it to sink down.
this.sinkDown(this.content.length - 1);
},
pop: function() {
// Store the first element so we can return it later.
var result = this.content[0];
// Get the element at the end of the array.
var end = this.content.pop();
// If there are any elements left, put the end element at the
// start, and let it bubble up.
if (this.content.length > 0) {
this.content[0] = end;
this.bubbleUp(0);
}
return result;
},
remove: function(node) {
var i = this.content.indexOf(node);
// When it is found, the process seen in 'pop' is repeated
// to fill up the hole.
var end = this.content.pop();
if (i !== this.content.length - 1) {
this.content[i] = end;
if (this.scoreFunction(end) < this.scoreFunction(node)) {
this.sinkDown(i);
} else {
this.bubbleUp(i);
}
}
},
size: function() {
return this.content.length;
},
rescoreElement: function(node) {
this.sinkDown(this.content.indexOf(node));
},
sinkDown: function(n) {
// Fetch the element that has to be sunk.
var element = this.content[n];
// When at 0, an element can not sink any further.
while (n > 0) {
// Compute the parent element's index, and fetch it.
var parentN = ((n + 1) >> 1) - 1;
var parent = this.content[parentN];
// Swap the elements if the parent is greater.
if (this.scoreFunction(element) < this.scoreFunction(parent)) {
this.content[parentN] = element;
this.content[n] = parent;
// Update 'n' to continue at the new position.
n = parentN;
}
// Found a parent that is less, no need to sink any further.
else {
break;
}
}
},
bubbleUp: function(n) {
// Look up the target element and its score.
var length = this.content.length;
var element = this.content[n];
var elemScore = this.scoreFunction(element);
while (true) {
// Compute the indices of the child elements.
var child2N = (n + 1) << 1;
var child1N = child2N - 1;
// This is used to store the new position of the element, if any.
var swap = null;
var child1Score;
// If the first child exists (is inside the array)...
if (child1N < length) {
// Look it up and compute its score.
var child1 = this.content[child1N];
child1Score = this.scoreFunction(child1);
// If the score is less than our element's, we need to swap.
if (child1Score < elemScore) {
swap = child1N;
}
}
// Do the same checks for the other child.
if (child2N < length) {
var child2 = this.content[child2N];
var child2Score = this.scoreFunction(child2);
if (child2Score < (swap === null ? elemScore : child1Score)) {
swap = child2N;
}
}
// If the element needs to be moved, swap it, and continue.
if (swap !== null) {
this.content[n] = this.content[swap];
this.content[swap] = element;
n = swap;
}
// Otherwise, we are done.
else {
break;
}
}
}
};
return {
astar: astar,
Graph: Graph,
gridDest: gridDest,
lngToGC: lngToGC,
latToGR: latToGR,
zoomStar: zoomStar,
gridBlack: gridBlack,
gridWhite: gridWhite
};
});