package squidpony.performance.alternate;
import squidpony.squidgrid.Direction;
import squidpony.squidmath.Coord;
import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
/**
* Performs A* search.
*
* A* is a best-first search algorithm for pathfinding. It uses a heuristic
* value to reduce the total search space. If the heuristic is too large then
* the optimal path is not guaranteed to be returned.
* <br>
* NOTE: Due to implementation problems, this class is atypically slow for A* and 34x slower than DijkstraMap on the
* same inputs. It may be improved in future versions, but for now you should strongly prefer DijkstraMap.
* @see squidpony.squidai.DijkstraMap a faster pathfinding algorithm with more features.
* @author Eben Howard - http://squidpony.com - howard@squidpony.com
*/
public class AStarSearch implements Serializable{
private static final long serialVersionUID = -6315495888417856297L;
/**
* The type of heuristic to use.
*/
public enum SearchType {
/**
* The distance it takes when only the four primary directions can be
* moved in.
*/
MANHATTAN,
/**
* The distance it takes when diagonal movement costs the same as
* cardinal movement.
*/
CHEBYSHEV,
/**
* The distance it takes as the crow flies.
*/
EUCLIDEAN,
/**
* Full space search. Least efficient but guaranteed to return a path if
* one exists. See also DijkstraMap class.
*/
DIJKSTRA
}
protected final double[][] map;
protected final HashSet<Coord> open = new HashSet<>();
protected final int width, height;
protected Coord[][] parent;
protected Coord start, target;
protected final SearchType type;
protected AStarSearch()
{
width = 0;
height = 0;
type = SearchType.MANHATTAN;
map = new double[width][height];
}
/**
* Builds a pathing object to run searches on.
*
* Values in the map are treated as positive values (and 0) being legal
* weights, with higher values being harder to pass through. Any negative
* value is treated as being an impassible space.
*
* If the type is Manhattan, only the cardinal directions will be used. All
* other search types will return results based on intercardinal and
* cardinal pathing.
*
* @param map
* the search map. It is not modified by this class, hence you can
* share this map among multiple instances.
* @param type the manner of search
*/
public AStarSearch(double[][] map, SearchType type) {
if (map == null)
throw new NullPointerException("map should not be null when building an AStarSearch");
this.map = map;
width = map.length;
height = width == 0 ? 0 : map[0].length;
if (type == null)
throw new NullPointerException("SearchType should not be null when building an AStarSearch");
this.type = type;
}
/**
* Finds an A* path to the target from the start. If no path is possible,
* returns null.
*
* @param startx the x coordinate of the start location
* @param starty the y coordinate of the start location
* @param targetx the x coordinate of the target location
* @param targety the y coordinate of the target location
* @return the shortest path, or null
*/
public Queue<Coord> path(int startx, int starty, int targetx, int targety) {
return path(Coord.get(startx, starty), Coord.get(targetx, targety));
}
/**
* Finds an A* path to the target from the start. If no path is possible,
* returns null.
*
* @param start the start location
* @param target the target location
* @return the shortest path, or null
*/
public Queue<Coord> path(Coord start, Coord target) {
this.start = start;
this.target = target;
open.clear();
boolean[][] finished = new boolean[width][height];
parent = new Coord[width][height];
Direction[] dirs;
switch (type) {
case MANHATTAN:
dirs = Direction.CARDINALS;
break;
case CHEBYSHEV:
case EUCLIDEAN:
case DIJKSTRA:
default:
dirs = Direction.OUTWARDS;
break;
}
Coord p = start;
open.add(p);
while (!p.equals(target)) {
finished[p.x][p.y] = true;
open.remove(p);
for (Direction dir : dirs) {
int x = p.x + dir.deltaX;
if (x < 0 || x >= width) {
continue;//out of bounds so skip ahead
}
int y = p.y + dir.deltaY;
if (y < 0 || y >= height) {
continue;//out of bounds so skip ahead
}
if (!finished[x][y]) {
Coord test = Coord.get(x, y);
if (open.contains(test)) {
double parentG = g(parent[x][y].x, parent[x][y].y);
if (parentG < 0) {
continue;//not a valid point so skip ahead
}
double g = g(p.x, p.y);
if (g < 0) {
continue;//not a valid point so skip ahead
}
if (parent[x][y] == null || parentG > g) {
parent[x][y] = p;
}
} else {
open.add(test);
parent[x][y] = p;
}
}
}
p = smallestF();
if (p == null) {
return null;//no path possible
}
}
/* Not using Deque nor ArrayDeque, they aren't Gwt compatible */
final LinkedList<Coord> deq = new LinkedList<>();
while (!p.equals(start)) {
deq.addFirst(p);
p = parent[p.x][p.y];
}
return deq;
}
/**
* Finds the g value (start to current) for the given location.
*
* If the given location is not valid or not attached to the pathfinding
* then -1 is returned.
*
* @param x coordinate
* @param y coordinate
*/
protected double g(int x, int y) {
if (x == start.x && y == start.y) {
return 0;
}
if (x < 0 || y < 0 || x >= width || y >= height || map[x][y] < 0 || parent[x][y] == null) {
return -1;//not a valid location
}
double parentG = g(parent[x][y].x, parent[x][y].y);
if (parentG < 0) {
return -1;//if any part of the path is not valid, this part is not valid
}
return map[x][y] + parentG + 1;//follow path back to start
}
/**
* Returns the heuristic distance from the current cell to the goal location\
* using the current calculation type.
*
* @param x coordinate
* @param y coordinate
* @return distance
*/
protected double h(int x, int y) {
switch (type) {
case MANHATTAN:
return Math.abs(x - target.x) + Math.abs(y - target.y);
case CHEBYSHEV:
return Math.max(Math.abs(x - target.x), Math.abs(y - target.y));
case EUCLIDEAN:
int xDist = Math.abs(x - target.x);
xDist *= xDist;
int yDist = Math.abs(y - target.y);
yDist *= yDist;
return Math.sqrt(xDist + yDist);
case DIJKSTRA:
default:
return 0;
}
}
/**
* Combines g and h to get the estimated distance from start to goal going on the current route.
* @param x coordinate
* @param y coordinate
* @return The current known shortest distance to the start position from
* the given position. If the current position cannot reach the
* start position or is invalid, -1 is returned.
*/
protected double f(int x, int y) {
double foundG = g(x, y);
if (foundG < 0) {
return -1;
}
return h(x, y) + foundG;
}
/**
* @return the current open point with the smallest F
*/
protected Coord smallestF() {
Coord smallest = null;
double smallF = Double.POSITIVE_INFINITY;
double f;
for (Coord p : open) {
f = f(p.x, p.y);
if (f < 0) {
continue;//current tested point is not valid so skip it
}
if (smallest == null || f < smallF) {
smallest = p;
smallF = f;
}
}
return smallest;
}
@Override
public String toString() {
final int width = map.length;
final int height = width == 0 ? 0 : map[0].length;
final StringBuilder result = new StringBuilder(width * height);
int maxLen = 0;
/*
* First we compute the longest (String-wise) entry, so that we can
* "indent" shorter cells, so that the output looks good (and is hereby
* readable).
*/
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final String output = String.valueOf(Math.round(map[x][y]));
final int locLen = output.length();
if (maxLen < locLen)
maxLen = locLen;
}
}
final String eol = System.getProperty("line.separator");
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final long v = Math.round(map[x][y]);
final String s = String.valueOf(v);
final int slen = s.length();
assert slen <= maxLen;
int diff = maxLen - slen;
while (0 < diff) {
result.append(" ");
diff--;
}
result.append(s);
}
if (y < height - 1)
result.append(eol);
}
return result.toString();
}
}