package squidpony.squidgrid;
import squidpony.annotation.GwtIncompatible;
import squidpony.squidmath.*;
import java.util.*;
/**
* Line of Sight (LOS) algorithms find if there is or is not a path between two
* given points.
* <br>
* The line found between two points will end at either the target, the
* obstruction closest to the start, or the edge of the map.
* <br>
* For normal line of sight usage, you should prefer Bresenham lines, and these
* are the default (they can also be specified by passing {@link #BRESENHAM} to
* the constructor). For more specialized usage, there are other kinds of LOS in
* this class, like lines that make no diagonal moves between cells (using
* {@link #ORTHO}, or lines that check a wide path (but these use different
* methods, like {@link #thickReachable(Radius)}).
* <br>
* Performance-wise, all of these methods are rather fast and about the same speed.
* {@link #RAY} is a tiny fraction faster than {@link #BRESENHAM} but produces
* rather low-quality lines in comparison. Calculating the visibility of 40,000
* lines in a 102x102 dungeon takes within 3% of 950ms (on an Intel i7-4700MQ laptop
* processor) for every one of BRESENHAM, DDA, ORTHO, and RAY, even with ORTHO
* finding a different kind of line by design.
*
* @author Eben Howard - http://squidpony.com - howard@squidpony.com
* @author Tommy Ettinger Added DDA, ORTHO, and the thick lines; some cleanup
* @author smelC optimized several methods
*/
public class LOS {
//constants to indicate desired type of solving algorithm to use
/**
* A Bresenham-based line-of-sight algorithm.
*/
public static final int BRESENHAM = 1;
/**
* Uses Wu's Algorithm as modified by Elias to draw the line. Does
* not end at an obstruction but rather returns one of the possible
* attempted paths in full.
*
* <p>
* Be aware, it is GWT-incompatible.
* </p>
*/
public static final int ELIAS = 2;
/**
* Uses a series of rays internal to the start and end point to
* determine visibility. Appearance is extremely close to DDA, which
* is also probably a faster algorithm, so BRESENHAM (which can look
* a little better) and DDA are recommended instead of RAY.
*/
public static final int RAY = 3;
/**
* Draws a line using only North/South/East/West movement.
*/
public static final int ORTHO = 4;
/**
* Optimized algorithm for Bresenham-like lines. There are slight
* differences in many parts of the lines this draws when compared
* to Bresenham lines, but it may also perform significantly better,
* and may also be useful as a building block for more complex LOS.
* Virtually identical in results to RAY, and just a hair slower, but
* better-tested and more predictable.
*/
public static final int DDA = 5;
/**
* Draws a line as if with a thick brush, going from a point between
* a corner of the starting cell and the center of the starting cell
* to the corresponding corner of the target cell, and considers the
* target visible if any portion of the thick stroke reached it. Will
* result in 1-width lines for exactly-orthogonal or exactly-diagonal
* lines and some parts of other lines, but usually is 2 cells wide.
*/
public static final int THICK = 6;
private LinkedList<Coord> lastPath = new LinkedList<>();
private int type;
private double[][] resistanceMap;
private int startx, starty, targetx, targety;
private Elias elias = null;
/**
* Gets the radius strategy this uses.
* @return the current Radius enum used to measure distance; starts at CIRCLE if not specified
*/
public Radius getRadiusStrategy() {
return radiusStrategy;
}
/**
* Set the radius strategy to the given Radius; the default is CIRCLE if this is not called.
* @param radiusStrategy a Radius enum to determine how distances are measured
*/
public void setRadiusStrategy(Radius radiusStrategy) {
this.radiusStrategy = radiusStrategy;
}
private Radius radiusStrategy = Radius.CIRCLE;
/**
* Constructs an LOS that will draw Bresenham lines and measure distances using the CIRCLE radius strategy.
*/
public LOS() {
this(BRESENHAM);
}
/**
* Constructs an LOS with the given type number, which must equal a static field in this class such as BRESENHAM.
* @param type an int that must correspond to the value of a static field in this class (such as BRESENHAM)
*/
public LOS(int type) {
this.type = type;
if(type == ELIAS)
elias = new Elias();
}
/**
* Returns true if a line can be drawn from the start point to the target
* point without intervening obstructions.
*
* Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with setRadiusStrategy .
*
* @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety) {
if(walls.length < 1) return false;
double[][] resMap = new double[walls.length][walls[0].length];
for(int x = 0; x < walls.length; x++)
{
for(int y = 0; y < walls[0].length; y++)
{
resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
}
}
return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy);
}
/**
* Returns true if a line can be drawn from the start point to the target
* point without intervening obstructions.
*
* Does not take into account resistance less than opaque or distance cost.
*
* Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with setRadiusStrategy .
*
* @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety) {
return isReachable(resistanceMap, startx, starty, targetx, targety, radiusStrategy);
}
/**
* Returns true if a line can be drawn from the start point to the target
* point without intervening obstructions.
*
* @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @param radiusStrategy the strategy to use in computing unit distance
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety, Radius radiusStrategy) {
if(resistanceMap.length < 1) return false;
this.resistanceMap = resistanceMap;
this.startx = startx;
this.starty = starty;
this.targetx = targetx;
this.targety = targety;
switch (type) {
case BRESENHAM:
return bresenhamReachable(radiusStrategy);
case ELIAS:
throw new IllegalStateException("Elias LOS is Gwt Incompatible");
// Comment required to compile with GWT:
// return eliasReachable(radiusStrategy);
case RAY:
return rayReachable(radiusStrategy);
case ORTHO:
return orthoReachable(radiusStrategy);
case DDA:
return ddaReachable(radiusStrategy);
case THICK:
return thickReachable(radiusStrategy);
}
return false;
}
/**
* Returns true if a line can be drawn from the start point to the target
* point without intervening obstructions.
*
* @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @param radiusStrategy the strategy to use in computing unit distance
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety, Radius radiusStrategy) {
if(walls.length < 1) return false;
double[][] resMap = new double[walls.length][walls[0].length];
for(int x = 0; x < walls.length; x++)
{
for(int y = 0; y < walls[0].length; y++)
{
resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
}
}
return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy);
}
/**
* Returns true if a line can be drawn from the any of the points within spread cells of the start point,
* to any of the corresponding points at the same direction and distance from the target point, without
* intervening obstructions. Primarily useful to paint a broad line that can be retrieved with getLastPath.
*
* @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @param radiusStrategy the strategy to use in computing unit distance
* @param spread the number of cells outward, measured by radiusStrategy, to place extra start and target points
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean spreadReachable(char[][] walls, int startx, int starty, int targetx, int targety, Radius radiusStrategy, int spread) {
if(walls.length < 1) return false;
resistanceMap = new double[walls.length][walls[0].length];
for(int x = 0; x < walls.length; x++)
{
for(int y = 0; y < walls[0].length; y++)
{
resistanceMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0;
}
}
this.startx = startx;
this.starty = starty;
this.targetx = targetx;
this.targety = targety;
return brushReachable(radiusStrategy, spread);
}
/**
* Returns true if a line can be drawn from the any of the points within spread cells of the start point,
* to any of the corresponding points at the same direction and distance from the target point, without
* intervening obstructions. Primarily useful to paint a broad line that can be retrieved with getLastPath.
*
* @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing.
* @param startx starting x position on the grid
* @param starty starting y position on the grid
* @param targetx ending x position on the grid
* @param targety ending y position on the grid
* @param radiusStrategy the strategy to use in computing unit distance
* @param spread the number of cells outward, measured by radiusStrategy, to place extra start and target points
* @return true if a line can be drawn without being obstructed, false otherwise
*/
public boolean spreadReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety, Radius radiusStrategy, int spread) {
if(resistanceMap.length < 1) return false;
this.resistanceMap = resistanceMap;
this.startx = startx;
this.starty = starty;
this.targetx = targetx;
this.targety = targety;
return brushReachable(radiusStrategy, spread);
}
/**
* Returns the path of the last LOS calculation, with the starting point as
* the head of the queue.
*
* @return
*/
public LinkedList<Coord> getLastPath() {
return lastPath;
}
/*
private boolean bresenhamReachable(Radius radiusStrategy) {
Queue<Coord> path = Bresenham.line2D(startx, starty, targetx, targety);
lastPath = new LinkedList<>();
lastPath.add(Coord.get(startx, starty));
double decay = 1 / radiusStrategy.radius(startx, starty, targetx, targety);
double currentForce = 1;
for (Coord p : path) {
lastPath.offer(p);
if (p.x == targetx && p.y == targety) {
return true;//reached the end
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
currentForce -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (currentForce - (r * decay) <= 0) {
return false;//too much resistance
}
}
return false;//never got to the target point
}
*/
private boolean bresenhamReachable(Radius radiusStrategy) {
Coord[] path = Bresenham.line2D_(startx, starty, targetx, targety);
lastPath = new LinkedList<>();
double rad = radiusStrategy.radius(startx, starty, targetx, targety);
if(rad == 0.0) {
lastPath.add(Coord.get(startx, starty));
return true; // already at the point; we can see our own feet just fine!
}
double decay = 1 / rad;
double currentForce = 1;
Coord p;
for (int i = 0; i < path.length; i++) {
p = path[i];
lastPath.offer(p);
if (p.x == targetx && p.y == targety) {
return true;//reached the end
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
currentForce -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (currentForce - (r * decay) <= 0) {
return false;//too much resistance
}
}
return false;//never got to the target point
}
private boolean orthoReachable(Radius radiusStrategy) {
Coord[] path = OrthoLine.line_(startx, starty, targetx, targety);
lastPath = new LinkedList<>();
double rad = radiusStrategy.radius(startx, starty, targetx, targety);
if(rad == 0.0) {
lastPath.add(Coord.get(startx, starty));
return true; // already at the point; we can see our own feet just fine!
}
double decay = 1 / rad;
double currentForce = 1;
Coord p;
for (int i = 0; i < path.length; i++) {
p = path[i];
lastPath.offer(p);
if (p.x == targetx && p.y == targety) {
return true;//reached the end
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
currentForce -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (currentForce - (r * decay) <= 0) {
return false;//too much resistance
}
}
return false;//never got to the target point
}
private boolean ddaReachable(Radius radiusStrategy) {
Coord[] path = DDALine.line_(startx, starty, targetx, targety);
lastPath = new LinkedList<>();
double rad = radiusStrategy.radius(startx, starty, targetx, targety);
if(rad == 0.0) {
lastPath.add(Coord.get(startx, starty));
return true; // already at the point; we can see our own feet just fine!
}
double decay = 1 / rad;
double currentForce = 1;
Coord p;
for (int i = 0; i < path.length; i++) {
p = path[i];
if (p.x == targetx && p.y == targety) {
lastPath.offer(p);
return true;//reached the end
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
currentForce -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (currentForce - (r * decay) <= 0) {
return false;//too much resistance
}
lastPath.offer(p);
}
return false;//never got to the target point
}
private boolean thickReachable(Radius radiusStrategy) {
lastPath = new LinkedList<>();
double dist = radiusStrategy.radius(startx, starty, targetx, targety);
double decay = 1.0 / dist; // note: decay can be positive infinity if dist is 0; this is actually OK
OrderedSet<Coord> visited = new OrderedSet<>((int) dist + 3);
List<List<Coord>> paths = new ArrayList<>(4);
/* // actual corners
paths.add(DDALine.line(startx, starty, targetx, targety, 0, 0));
paths.add(DDALine.line(startx, starty, targetx, targety, 0, 0xffff));
paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0));
paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0xffff));
*/
// halfway between the center and a corner
paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0x3fff));
paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0xbfff));
paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0x3fff));
paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0xbfff));
int length = Math.max(paths.get(0).size(), Math.max(paths.get(1).size(),
Math.max(paths.get(2).size(), paths.get(3).size())));
double[] forces = new double[]{1,1,1,1};
boolean[] go = new boolean[]{true, true, true, true};
Coord p;
for (int d = 0; d < length; d++) {
for (int pc = 0; pc < 4; pc++) {
List<Coord> path = paths.get(pc);
if(d < path.size() && go[pc])
p = path.get(d);
else continue;
if (p.x == targetx && p.y == targety) {
visited.add(p);
lastPath.addAll(visited);
return true;//reached the end
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
forces[pc] -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (forces[pc] - (r * decay) <= 0) {
go[pc] = false;
continue;//too much resistance
}
visited.add(p);
}
}
lastPath.addAll(visited);
return false;//never got to the target point
}
private boolean brushReachable(Radius radiusStrategy, int spread) {
lastPath = new LinkedList<>();
double dist = radiusStrategy.radius(startx, starty, targetx, targety) + spread * 2, decay = 1 / dist;
OrderedSet<Coord> visited = new OrderedSet<>((int) (dist + 3) * spread);
List<List<Coord>> paths = new ArrayList<>((int) (radiusStrategy.volume2D(spread) * 1.25));
int length = 0;
List<Coord> currentPath;
for (int i = -spread; i <= spread; i++) {
for (int j = -spread; j <= spread; j++) {
if(radiusStrategy.inRange(startx, starty, startx + i, starty + j, 0, spread)
&& startx + i >= 0 && starty + j >= 0
&& startx + i < resistanceMap.length && starty + j < resistanceMap[0].length
&& targetx + i >= 0 && targety + j >= 0
&& targetx + i < resistanceMap.length && targety + j < resistanceMap[0].length) {
for (int q = 0x3fff; q < 0xffff; q += 0x8000) {
for (int r = 0x3fff; r < 0xffff; r += 0x8000) {
currentPath = DDALine.line(startx+i, starty+j, targetx+i, targety+j, q, r);
paths.add(currentPath);
length = Math.max(length, currentPath.size());
}
}
}
}
}
double[] forces = new double[paths.size()];
Arrays.fill(forces, 1.0);
boolean[] go = new boolean[paths.size()];
Arrays.fill(go, true);
Coord p;
boolean found = false;
for (int d = 0; d < length; d++) {
for (int pc = 0; pc < paths.size(); pc++) {
List<Coord> path = paths.get(pc);
if(d < path.size() && go[pc])
p = path.get(d);
else continue;
if (p.x == targetx && p.y == targety) {
found = true;
}
if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
forces[pc] -= resistanceMap[p.x][p.y];
}
double r = radiusStrategy.radius(startx, starty, p.x, p.y);
if (forces[pc] - (r * decay) <= 0) {
go[pc] = false;
continue;//too much resistance
}
visited.add(p);
}
}
lastPath.addAll(visited);
return found;//never got to the target point
}
private boolean rayReachable(Radius radiusStrategy) {
lastPath = new LinkedList<>();//save path for later retrieval
if (startx == targetx && starty == targety) {//already there!
lastPath.add(Coord.get(startx, starty));
return true;
}
int width = resistanceMap.length;
int height = resistanceMap[0].length;
Coord end = Coord.get(targetx, targety);
//find out which direction to step, on each axis
int stepX = (int) Math.signum(targetx - startx),
stepY = (int) Math.signum(targety - starty);
int deltaY = Math.abs(targetx - startx),
deltaX = Math.abs(targety - starty);
int testX = startx,
testY = starty;
int maxX = deltaX,
maxY = deltaY;
while (testX >= 0 && testX < width && testY >= 0 && testY < height && (testX != targetx || testY != targety)) {
lastPath.add(Coord.get(testX, testY));
if (maxY - maxX > deltaX) {
maxX += deltaX;
testX += stepX;
if (resistanceMap[testX][testY] >= 1f) {
end = Coord.get(testX, testY);
break;
}
} else if (maxX - maxY > deltaY) {
maxY += deltaY;
testY += stepY;
if (resistanceMap[testX][testY] >= 1f) {
end = Coord.get(testX, testY);
break;
}
} else {//directly on diagonal, move both full step
maxY += deltaY;
testY += stepY;
maxX += deltaX;
testX += stepX;
if (resistanceMap[testX][testY] >= 1f) {
end = Coord.get(testX, testY);
break;
}
}
if (radiusStrategy.radius(testX, testY, startx, starty) > radiusStrategy.radius(startx, starty, end.x, end.y)) {//went too far
break;
}
}
if (end.x >= 0 && end.x < width && end.y >= 0 && end.y < height) {
lastPath.add(Coord.get(end.x, end.y));
}
return end.x == targetx && end.y == targety;
}
@GwtIncompatible /* Because of Thread */
private boolean eliasReachable(Radius radiusStrategy) {
if(elias == null)
elias = new Elias();
List<Coord> ePath = elias.line(startx, starty, targetx, targety);
lastPath = new LinkedList<>(ePath);//save path for later retreival
HashMap<EliasWorker, Thread> pool = new HashMap<>();
for (Coord p : ePath) {
EliasWorker worker = new EliasWorker(p.x, p.y, radiusStrategy);
Thread thread = new Thread(worker);
thread.start();
pool.put(worker, thread);
}
for (EliasWorker w : pool.keySet()) {
try {
pool.get(w).join();
} catch (InterruptedException ex) {
}
if (w.succeeded) {
lastPath = w.path;
return true;
}
}
return false;//never got to the target point
}
private class EliasWorker implements Runnable {
private LinkedList<Coord> path;
private boolean succeeded = false;
private int testx, testy;
private Radius eliasRadiusStrategy;
EliasWorker(int testx, int testy, Radius radiusStrategy) {
this.testx = testx;
this.testy = testy;
this.eliasRadiusStrategy = radiusStrategy;
}
@Override
public void run() {
LOS los1 = new LOS(BRESENHAM);
LOS los2 = new LOS(BRESENHAM);
//if a non-solid midpoint on the path can see both the start and end, consider the two ends to be able to see each other
if (resistanceMap[testx][testy] < 1
&& eliasRadiusStrategy.radius(startx, starty, testx, testy) <= eliasRadiusStrategy.radius(startx, starty, targetx, targety)
&& los1.isReachable(resistanceMap, testx, testy, targetx, targety, eliasRadiusStrategy)
&& los2.isReachable(resistanceMap, startx, starty, testx, testy, eliasRadiusStrategy)) {
//record actual sight path used
path = new LinkedList<>(los2.lastPath);
path.addAll(los1.lastPath);
succeeded = true;
}
}
}
}