Skip to content

Commit e510dd8

Browse files
committed
Ability to have non-grid layouts
- Extract grid specificities into the Graph function (thus replace `grid` parameter to a `graph`object) - Regroup heuristics functions under astar.heuristics - Update tests, benchmark & demo - Added a specific test to demonstrate non-grid layout usage with a gps heuristic
1 parent e060a37 commit e510dd8

File tree

4 files changed

+173
-76
lines changed

4 files changed

+173
-76
lines changed

astar.js

Lines changed: 85 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,16 @@
1818
})(function() {
1919

2020
var astar = {
21-
init: function(grid) {
22-
for(var x = 0, xl = grid.length; x < xl; x++) {
23-
for(var y = 0, yl = grid[x].length; y < yl; y++) {
24-
var node = grid[x][y];
25-
node.f = 0;
26-
node.g = 0;
27-
node.h = 0;
28-
node.cost = node.type;
29-
node.visited = false;
30-
node.closed = false;
31-
node.parent = null;
32-
}
21+
init: function(graph) {
22+
for (var i = 0, len = graph.nodes.length; i < len; ++i) {
23+
var node = graph.nodes[i];
24+
node.f = 0;
25+
node.g = 0;
26+
node.h = 0;
27+
node.cost = node.type;
28+
node.visited = false;
29+
node.closed = false;
30+
node.parent = null;
3331
}
3432
},
3533
heap: function() {
@@ -42,24 +40,22 @@ var astar = {
4240
// supported options:
4341
// {
4442
// heuristic: heuristic function to use
45-
// diagonal: boolean specifying whether diagonal moves are allowed
4643
// closest: boolean specifying whether to return closest node if
4744
// target is unreachable
4845
// }
49-
search: function(grid, start, end, options) {
50-
astar.init(grid);
46+
search: function(graph, start, end, options) {
47+
astar.init(graph);
5148

5249
options = options || {};
53-
var heuristic = options.heuristic || astar.manhattan;
54-
var diagonal = !!options.diagonal;
50+
var heuristic = options.heuristic || astar.heuristics.manhattan;
5551
var closest = options.closest || false;
5652

5753
var openHeap = astar.heap();
5854

5955
// set the start node to be the closest if required
6056
var closestNode = start;
6157

62-
start.h = heuristic(start.pos, end.pos);
58+
start.h = heuristic(start, end);
6359

6460
function pathTo(node){
6561
var curr = node;
@@ -87,8 +83,8 @@ var astar = {
8783
// Normal case -- move currentNode from open to closed, process each of its neighbors.
8884
currentNode.closed = true;
8985

90-
// Find all neighbors for the current node. Optionally find diagonal neighbors as well (false by default).
91-
var neighbors = astar.neighbors(grid, currentNode, diagonal);
86+
// Find all neighbors for the current node.
87+
var neighbors = graph.neighbors(currentNode);
9288

9389
for(var i=0, il = neighbors.length; i < il; i++) {
9490
var neighbor = neighbors[i];
@@ -108,7 +104,7 @@ var astar = {
108104
// Found an optimal (so far) path to this node. Take score for node to see how good it is.
109105
neighbor.visited = true;
110106
neighbor.parent = currentNode;
111-
neighbor.h = neighbor.h || heuristic(neighbor.pos, end.pos);
107+
neighbor.h = neighbor.h || heuristic(neighbor, end);
112108
neighbor.g = gScore;
113109
neighbor.f = neighbor.g + neighbor.h;
114110

@@ -141,24 +137,70 @@ var astar = {
141137
// No result was found - empty array signifies failure to find path.
142138
return [];
143139
},
144-
manhattan: function(pos0, pos1) {
145-
// See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
140+
// See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
141+
heuristics: {
142+
manhattan: function(pos0, pos1) {
143+
var d1 = Math.abs (pos1.x - pos0.x);
144+
var d2 = Math.abs (pos1.y - pos0.y);
145+
return d1 + d2;
146+
},
147+
diagonal: function(pos0, pos1) {
148+
var D = 1;
149+
var D2 = Math.sqrt(2);
150+
var d1 = Math.abs (pos1.x - pos0.x);
151+
var d2 = Math.abs (pos1.y - pos0.y);
152+
return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
153+
},
154+
gps: function(node0, node1) {
155+
var x = (node1.longRad - node0.longRad) * Math.cos((node0.latRad + node1.latRad)/2),
156+
y = node1.latRad - node0.latRad,
157+
res = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) * 6371;
158+
//printErr("Distance from " + node0.name + " to " + node1.name + " = " + res);
159+
return res;
160+
}
161+
}
162+
};
146163

147-
var d1 = Math.abs (pos1.x - pos0.x);
148-
var d2 = Math.abs (pos1.y - pos0.y);
149-
return d1 + d2;
150-
},
151-
diagonal: function(pos0, pos1) {
152-
var D = 1;
153-
var D2 = Math.sqrt(2);
154-
var d1 = Math.abs (pos1.x - pos0.x);
155-
var d2 = Math.abs (pos1.y - pos0.y);
156-
return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
157-
},
158-
neighbors: function(grid, node, diagonals) {
159-
var ret = [];
160-
var x = node.x;
161-
var y = node.y;
164+
/**
165+
* A graph memory structure
166+
* @param {Array} [gridIn] Facultative grid of input weights
167+
* @param {bool} [diagonal] Specify whether diagonal moves are allowed
168+
*/
169+
function Graph(gridIn, diagonal) {
170+
var nodes = [];
171+
172+
if (gridIn) {
173+
var grid = [],
174+
node;
175+
for (var x = 0; x < gridIn.length; x++) {
176+
grid[x] = [];
177+
178+
for (var y = 0, row = gridIn[x]; y < row.length; y++) {
179+
node = new GraphNode(x, y, row[y]);
180+
grid[x][y] = node;
181+
nodes.push(node);
182+
}
183+
}
184+
this.grid = grid;
185+
}
186+
187+
this.nodes = nodes;
188+
this.diagonal = !!diagonal; // Optionally find diagonal neighbors as well (false by default).
189+
}
190+
191+
Graph.prototype.add = function(node) {
192+
if (this.nodes.indexOf(node) == -1) {
193+
this.nodes.push(node);
194+
}
195+
};
196+
197+
Graph.prototype.neighbors = function(node) {
198+
var ret = [],
199+
x = node.x,
200+
y = node.y;
201+
202+
if (this.grid) {
203+
var grid = this.grid;
162204

163205
// West
164206
if(grid[x-1] && grid[x-1][y]) {
@@ -180,8 +222,7 @@ var astar = {
180222
ret.push(grid[x][y+1]);
181223
}
182224

183-
if (diagonals) {
184-
225+
if (this.diagonal) {
185226
// Southwest
186227
if(grid[x-1] && grid[x-1][y-1]) {
187228
ret.push(grid[x-1][y-1]);
@@ -201,27 +242,14 @@ var astar = {
201242
if(grid[x+1] && grid[x+1][y+1]) {
202243
ret.push(grid[x+1][y+1]);
203244
}
204-
205245
}
206-
207-
return ret;
208246
}
209-
};
210-
211-
function Graph(grid) {
212-
var nodes = [];
213-
214-
for (var x = 0; x < grid.length; x++) {
215-
nodes[x] = [];
216-
217-
for (var y = 0, row = grid[x]; y < row.length; y++) {
218-
nodes[x][y] = new GraphNode(x, y, row[y]);
219-
}
247+
else {
248+
// Your neighbors implementation!
220249
}
221250

222-
this.input = grid;
223-
this.nodes = nodes;
224-
}
251+
return ret;
252+
};
225253

226254
Graph.prototype.toString = function() {
227255
var graphString = "\n";
@@ -239,13 +267,8 @@ Graph.prototype.toString = function() {
239267
};
240268

241269
function GraphNode(x, y, type) {
242-
this.data = { };
243270
this.x = x;
244271
this.y = y;
245-
this.pos = {
246-
x: x,
247-
y: y
248-
};
249272
this.type = type;
250273
}
251274

benchmark/benchmark.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ $(function() {
77
running = true;
88

99
var graph = new Graph(grid);
10-
var start = graph.nodes[0][0];
11-
var end = graph.nodes[140][140];
10+
var start = graph.grid[0][0];
11+
var end = graph.grid[140][140];
1212
var results = [];
1313
var times = 0;
1414

1515
for (var i = 0; i < 1000; i++) {
1616
var startTime = new Date().getTime();
17-
var result = astar.search(graph.nodes, start, end);
17+
var result = astar.search(graph, start, end);
1818
var endTime = new Date().getTime();
1919
times = times + (endTime - startTime);
2020

demo/demo.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ $(function() {
7777
});
7878

7979
$searchDiagonal.change(function() {
80-
grid.setOption({diagonal: $(this).is(":checked")});
80+
var val = $(this).is(":checked");
81+
grid.setOption({diagonal: val});
82+
grid.graph.diagonal = val;
8183
});
8284

8385
$checkClosest.change(function() {
@@ -178,8 +180,7 @@ GraphSearch.prototype.cellClicked = function($end) {
178180
var start = this.nodeFromElement($start);
179181

180182
var sTime = new Date();
181-
var path = this.search(this.graph.nodes, start, end, {
182-
diagonal: this.opts.diagonal,
183+
var path = this.search(this.graph, start, end, {
183184
closest: this.opts.closest
184185
});
185186
var fTime = new Date();
@@ -215,7 +216,7 @@ GraphSearch.prototype.drawDebugInfo = function(show) {
215216
}
216217
};
217218
GraphSearch.prototype.nodeFromElement = function($cell) {
218-
return this.graph.nodes[parseInt($cell.attr("x"))][parseInt($cell.attr("y"))];
219+
return this.graph.grid[parseInt($cell.attr("x"))][parseInt($cell.attr("y"))];
219220
};
220221
GraphSearch.prototype.animateNoPath = function() {
221222
var $graph = this.$graph;

test/tests.js

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ test( "Pathfinding to closest", function() {
8080

8181
function runSearch(grid, start, end, options) {
8282
var graph = new Graph(grid);
83-
var start = graph.nodes[start[0]][start[1]];
84-
var end = graph.nodes[end[0]][end[1]];
83+
var start = graph.grid[start[0]][start[1]];
84+
var end = graph.grid[end[0]][end[1]];
8585
var sTime = new Date();
86-
var result = astar.search(graph.nodes, start, end, options);
86+
var result = astar.search(graph, start, end, options);
8787
var eTime = new Date();
8888
return {
8989
result: result,
@@ -94,20 +94,93 @@ function runSearch(grid, start, end, options) {
9494

9595
function pathToString(result) {
9696
return result.map(function(node) {
97-
return "(" + node.pos.x + "," + node.pos.y + ")";
97+
return "(" + node.x + "," + node.y + ")";
9898
}).join("");
9999
}
100100

101+
test( "GPS Pathfinding", function() {
102+
var data = [
103+
{name: "Paris", lat: 48.8567, lng: 2.3508},
104+
{name: "Lyon", lat: 45.76, lng: 4.84},
105+
{name: "Marseille", lat: 43.2964, lng: 5.37},
106+
{name: "Bordeaux", lat: 44.84, lng: -0.58},
107+
{name: "Cannes", lat: 43.5513, lng: 7.0128},
108+
{name: "Toulouse", lat: 43.6045, lng: 1.444},
109+
{name: "Reims", lat: 49.2628, lng: 4.0347}
110+
],
111+
links = {
112+
"Paris": ["Lyon", "Bordeaux", "Reims"],
113+
"Lyon": ["Paris", "Marseille"],
114+
"Marseille": ["Lyon", "Cannes", "Toulouse"],
115+
"Bordeaux": ["Toulouse", "Paris"],
116+
"Cannes": ["Marseille"],
117+
"Toulouse": ["Marseille", "Bordeaux"],
118+
"Reims": ["Paris"]
119+
};
120+
121+
function CityNode(name, lat, lng) {
122+
this.name = name;
123+
this.lat = lat;
124+
this.lng = lng;
125+
this.longRad = this.lng * Math.PI / 180;
126+
this.latRad = this.lat * Math.PI / 180;
127+
}
128+
CityNode.prototype.type = 1;
129+
CityNode.prototype.toString = function() {
130+
return "[" + this.name + " (" + this.lat + ", " + this.lng + ")]";
131+
};
132+
CityNode.prototype.isWall = function() {
133+
return this.weight === 0;
134+
};
135+
136+
//---
137+
138+
var graph = new Graph(),
139+
cities = {};
140+
for (var i = 0; i < data.length; ++i) {
141+
var city = data[i],
142+
obj = new CityNode(city.name, city.lat, city.lng);
143+
graph.add(obj);
144+
cities[obj.name] = obj;
145+
}
146+
147+
graph.cities = cities;
148+
graph.links = links;
149+
150+
var GPSheuristic = astar.heuristics.gps;
151+
152+
graph.neighbors = function (node) { // Override neighbors function for this specific graph
153+
var neighbors = [],
154+
ids = this.links[node.name];
155+
for (var i = 0, len = ids.length; i < len; ++i) {
156+
var name = ids[i],
157+
neighbor = this.cities[name];
158+
neighbor.cost = GPSheuristic(node, neighbor); // Compute real cost!
159+
neighbors.push(neighbor);
160+
}
161+
return neighbors;
162+
};
163+
164+
var start = cities["Paris"],
165+
end = cities["Cannes"];
166+
167+
var result = astar.search(graph, start, end, {heuristic: GPSheuristic});
168+
equal(result.length, 3, "Cannes is 3 cities away from Paris");
169+
equal(result[0].name, "Lyon", "City #1 is Lyon");
170+
equal(result[1].name, "Marseille", "City #2 is Marseille");
171+
equal(result[2].name, "Cannes", "City #3 is Cannes");
172+
});
173+
101174
// // https://gist.github.com/bgrins/581352
102175
// function runBasic() {
103176
// var graph = new Graph([
104177
// [1,1,1,1],
105178
// [0,1,1,0],
106179
// [0,0,1,1]
107180
// ]);
108-
// var start = graph.nodes[0][0];
109-
// var end = graph.nodes[1][2];
110-
// var result = astar.search(graph.nodes, start, end);
181+
// var start = graph.grid[0][0];
182+
// var end = graph.grid[1][2];
183+
// var result = astar.search(graph, start, end);
111184

112185
// return "<pre>" + result.join(", ") + "</pre>";
113186
// }

0 commit comments

Comments
 (0)