Skip to content

add ThetaStar #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Currently there are 10 path-finders bundled in this library, namely:
* `IDAStarFinder.js` *
* `JumpPointFinder` *
* `OrthogonalJumpPointFinder` *
* `ThetaStarFinder` *
* `BiAStarFinder`
* `BiBestFirstFinder`
* `BiBreadthFirstFinder` *
Expand Down
1 change: 1 addition & 0 deletions src/PathFinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ module.exports = {
'BiDijkstraFinder' : require('./finders/BiDijkstraFinder'),
'IDAStarFinder' : require('./finders/IDAStarFinder'),
'JumpPointFinder' : require('./finders/JumpPointFinder'),
'ThetaStarFinder' : require('./finders/ThetaStarFinder'),
};
240 changes: 240 additions & 0 deletions src/finders/ThetaStarFinder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
var Heap = require('heap');
var Util = require('../core/Util');
var Heuristic = require('../core/Heuristic');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Theta* path-finder.
* @constructor
* @param {object} opt
*/
function ThetaStarFinder(opt) {
opt = opt || {};
this.allowDiagonal = true;
this.dontCrossCorners = true;
this.heuristic = Heuristic.euclidean;
this.weight = opt.weight || 1.1;
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
}

ThetaStarFinder.prototype.distance = function(startX, startY, endX, endY) {
startX -= endX;
startY -= endY;
return Math.sqrt(startX * startX + startY * startY);
}

ThetaStarFinder.prototype.lineOfSight = function(startX, startY, endX, endY, grid) {
var sx, sy, f, s0, s1,
x0 = startX,
y0 = startY,
x1 = endX,
y1 = endY,
dx = x1 - x0,
dy = y1 - y0;
if (dx < 0) {
dx = -dx;
sx = -1;
} else {
sx = 1;
}
if (dy < 0) {
dy = -dy;
sy = -1;
} else {
sy = 1;
}
if (dx == 0) {
for (y0 += sy; y0 != y1; y0 += sy) {
if (!grid.isWalkableAt(x0, y0)) {
return false;
}
}
return true;
}
if (dy == 0) {
for (x0 += sx; x0 != x1; x0 += sx) {
if (!grid.isWalkableAt(x0, y0)) {
return false;
}
}
return true;
}
if (dx >= dy) {
if (!grid.isWalkableAt(x0, y0 + sy) || !grid.isWalkableAt(x1, y1 - sy)) {
return false;
}
for (s0 = y0, s1 = y1, f = dy; ;) {
f += dy;
if (f >= dx) {
x0 += sx;
y0 += sy;
x1 -= sx;
y1 -= sy;
f -= dx;
} else {
x0 += sx;
x1 -= sx;
}
if (x0 == x1 + sx) {
break;
}
if (x0 == x1) {
if (y0 == y1) {
if (!grid.isWalkableAt(x0, y0 - sy) || !grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x0, y0 + sy)) {
return false;
}
} else {
if (!grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x1, y1)) {
return false;
}
}
break;
}
if (y0 != s0 && !grid.isWalkableAt(x0, y0 - sy)) {
return false;
}
if (!grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x0, y0 + sy)) {
return false;
}
if (y1 != s1 && !grid.isWalkableAt(x1, y1 + sy)) {
return false;
}
if (!grid.isWalkableAt(x1, y1) || !grid.isWalkableAt(x1, y1 - sy)) {
return false;
}
}
}
else {
if (!grid.isWalkableAt(x0 + sx, y0) || !grid.isWalkableAt(x1 - sx, y1)) {
return false;
}
for (s0 = x0, s1 = x1, f = dx; ;) {
f += dx;
if (f >= dy) {
x0 += sx;
y0 += sy;
x1 -= sx;
y1 -= sy;
f -= dy;
} else {
y0 += sy;
y1 -= sy;
}
if (y0 == y1 + sy) {
break;
}
if (y0 == y1) {
if (x0 == x1) {
if (!grid.isWalkableAt(x0 - sx, y0) || !grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x0 + sx, y0)) {
return false;
}
} else {
if (!grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x1, y1)) {
return false;
}
}
break;
}
if (x0 != s0 && !grid.isWalkableAt(x0 - sx, y0)) {
return false;
}
if (!grid.isWalkableAt(x0, y0) || !grid.isWalkableAt(x0 + sx, y0)) {
return false;
}
if (x1 != s1 && !grid.isWalkableAt(x1 + sx, y1)) {
return false;
}
if (!grid.isWalkableAt(x1, y1) || !grid.isWalkableAt(x1 - sx, y1)) {
return false;
}
}
}
return true;
}

/**
* Find and return the the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
ThetaStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
var openList = new Heap(function(nodeA, nodeB) {
return nodeA.f - nodeB.f;
}),
startNode = grid.getNodeAt(startX, startY),
endNode = grid.getNodeAt(endX, endY),
distance = this.distance,
lineOfSight = this.lineOfSight,
diagonalMovement = this.diagonalMovement,
weight = this.weight,
node, neighbors, neighbor, i, l, x, y, ng;

// set the `g` and `f` value of the start node to be 0
startNode.g = 0;
startNode.f = 0;

// push the start node into the open list
openList.push(startNode);
startNode.opened = true;

// while the open list is not empty
while (!openList.empty()) {
// pop the position of node which has the minimum `f` value.
node = openList.pop();
node.closed = true;

// if reached the end position, construct the path and return it
if (node === endNode) {
return Util.backtrace(endNode);
}

// get neigbours of the current node
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];

if (neighbor.closed) {
continue;
}
x = neighbor.x;
y = neighbor.y;

// check if the neighbor has not been inspected yet, or
// can be reached with smaller cost from the current node
if (node.parent && lineOfSight(x, y, node.parent.x, node.parent.y, grid)) {
ng = node.parent.g + distance(x, y, node.parent.x, node.parent.y);
if (!neighbor.opened || ng < neighbor.g) {
neighbor.g = ng;
neighbor.parent = node.parent;
} else {
continue;
}
}
else {
ng = node.g + distance(x, y, node.x, node.y);
if (!neighbor.opened || ng < neighbor.g) {
neighbor.g = ng;
neighbor.parent = node;
} else {
continue;
}
}
neighbor.h = neighbor.h || weight * distance(x, y, endX, endY);
neighbor.f = neighbor.g + neighbor.h;

if (!neighbor.opened) {
openList.push(neighbor);
neighbor.opened = true;
} else {
// the neighbor can be reached with smaller cost.
// Since its f value has been updated, we have to
// update its position in the open list
openList.updateItem(neighbor);
}
} // end for each neighbor
} // end while not open list empty

// fail to find the path
return [];
};

module.exports = ThetaStarFinder;
7 changes: 6 additions & 1 deletion test/PathTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ pathTests({
});

// finders NOT guaranteed to find the shortest path
// ThetaStar guaranteed to find the shortest path , but it's not compatible to our test ...
pathTests({
name: 'ThetaStar',
finder: new PF.ThetaStarFinder(),
optimal: false
}, {
name: 'BiAStar',
finder: new PF.BiAStarFinder(),
optimal: false
Expand All @@ -103,7 +108,7 @@ pathTests({
diagonalMovement: PF.DiagonalMovement.IfAtMostOneObstacle
}),
optimal: false
}, {
}, {
name: 'JPFNeverMoveDiagonally',
finder: new PF.JumpPointFinder({
diagonalMovement: PF.DiagonalMovement.Never
Expand Down
26 changes: 19 additions & 7 deletions visual/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ <h3>Options</h3>
<label class="option_label">Allow Diagonal</label> <br>
<input type="checkbox" class="bi-directional">
<label class="option_label">Bi-directional</label> <br>
<input type="checkbox" class="dont_cross_corners">
<input type="checkbox" class="dont_cross_corners" checked>
<label class="option_label">Don't Cross Corners</label> <br>
<input class="spinner" name="astar_weight" value="1">
<label class="option_label">Weight</label> <br>
Expand Down Expand Up @@ -98,9 +98,9 @@ <h3>Options</h3>
<div class="optional sub_options">
<input type="checkbox" class="allow_diagonal" checked>
<label class="option_label">Allow Diagonal</label> <br>
<input type="checkbox" class="dont_cross_corners">
<input type="checkbox" class="dont_cross_corners" checked>
<label class="option_label">Don't Cross Corners</label> <br>
<input class="spinner" name="astar_weight" value="1">
<input class="spinner" name="ida_weight" value="1">
<label class="option_label">Weight</label> <br>
<input class="spinner" name="time_limit" value="10">
<label class="option_label">Seconds limit</label> <br>
Expand All @@ -119,7 +119,7 @@ <h3>Options</h3>
<label class="option_label">Allow Diagonal</label> <br>
<input type="checkbox" class="bi-directional">
<label class="option_label">Bi-directional</label> <br>
<input type="checkbox" class="dont_cross_corners">
<input type="checkbox" class="dont_cross_corners" checked>
<label class="option_label">Don't Cross Corners</label> <br>
</div>
</div>
Expand Down Expand Up @@ -148,7 +148,7 @@ <h3>Options</h3>
<label class="option_label">Allow Diagonal</label> <br>
<input type="checkbox" class="bi-directional">
<label class="option_label">Bi-directional</label> <br>
<input type="checkbox" class="dont_cross_corners">
<input type="checkbox" class="dont_cross_corners" checked>
<label class="option_label">Don't Cross Corners</label> <br>
</div>
</div>
Expand All @@ -163,7 +163,7 @@ <h3>Options</h3>
<label class="option_label">Allow Diagonal</label> <br>
<input type="checkbox" class="bi-directional">
<label class="option_label">Bi-directional</label> <br>
<input type="checkbox" class="dont_cross_corners">
<input type="checkbox" class="dont_cross_corners" checked>
<label class="option_label">Don't Cross Corners</label> <br>
</div>
</div>
Expand Down Expand Up @@ -192,7 +192,7 @@ <h3>Options</h3>
</div>
</div>

<h3 id="orth_jump_point_header"><a href="#">Orthogonal Jump Point Search</a></h3>
<h3 id="orth_jump_point_header"><a href="#">Orthogonal Jump Point Search</a></h3>
<div id="orth_jump_point_section" class="finder_section">
<header class="option_header">
<h3>Heuristic</h3>
Expand All @@ -215,6 +215,18 @@ <h3>Options</h3>
<label class="option_label">Visualize recursion</label> <br>
</div>
</div>

<h3 id="thetastar_header"><a href="#">Theta*</a></h3>
<div id="thetastar_section" class="finder_section">

<header class="option_header">
<h3>Options</h3>
</header>
<div class="optional sub_options">
<input class="spinner" name="thetastar_weight" value="1.1">
<label class="option_label">Weight</label> <br>
</div>
</div>

</div><!-- .accordion -->
</div><!-- #algorithm_panel -->
Expand Down
13 changes: 12 additions & 1 deletion visual/js/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ var Panel = {
diagonalMovement: PF.DiagonalMovement.IfAtMostOneObstacle
});
break;

case 'orth_jump_point_header':
trackRecursion = typeof $('#orth_jump_point_section ' +
'.track_recursion:checked').val() !== 'undefined';
Expand All @@ -149,6 +150,7 @@ var Panel = {
diagonalMovement: PF.DiagonalMovement.Never
});
break;

case 'ida_header':
allowDiagonal = typeof $('#ida_section ' +
'.allow_diagonal:checked').val() !== 'undefined';
Expand All @@ -159,7 +161,7 @@ var Panel = {

heuristic = $('input[name=jump_point_heuristic]:checked').val();

weight = parseInt($('#ida_section input[name=astar_weight]').val()) || 1;
weight = parseInt($('#ida_section input[name=ida_weight]').val()) || 1;
weight = weight >= 1 ? weight : 1; /* if negative or 0, use 1 */

timeLimit = parseInt($('#ida_section input[name=time_limit]').val());
Expand All @@ -177,6 +179,15 @@ var Panel = {
});

break;

case 'thetastar_header':
weight = parseFloat($('#thetastar_section input[name=thetastar_weight]').val()) || 1.1;
weight = weight >= 1 ? weight : 1; /* if negative or 0, use 1 */

finder = new PF.ThetaStarFinder({
weight: weight
});
break;
}

return finder;
Expand Down
2 changes: 1 addition & 1 deletion visual/lib/pathfinding-browser.min.js

Large diffs are not rendered by default.