I have a program where an entity moves around in two-dimensional space. To move one step, the entity picks its next point, and then sets it as his current point.
Sometimes, however, the entity's next point lies in an Area
(java.awt.geom.Area) that is forbidden (the "forbidden area" is actually a velocity obstacle).
How can the entity pick the point outside the Area
which is closest to the entity's preferred point?
The Area
is composed of different shapes (sometimes, the shapes are not touching).
My initial plan was to simply draw a line to the preferred point. Wherever the line intersected the Area
first, this would be the next-best point. However, finding the intersection between a line and an Area
turns out to be quite complex.
EDIT: This wouldn't necessarily find the closest point. This would just find the closet point on the same trajectory. I'm looking for the closest possible point.
Perhaps Area
isn't the best class to use. All I require is something that can add multiple shapes, even when the shapes aren't touching.
I've solved the problem:
First, find all the line segments that constrain the Area
. I've written code to do that on a different answer.
Then, it's just a matter of iterating through each line segment, and recording the point on the segment that's closest to the entity's desired point. Store these in the data structure of your choice (e.g., an ArrayList
).
See: Shortest distance between a point and a line segment
Lastly, determine which of the points is closest to the desired point. Voilà!
Here's a demonstration:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JFrame;
public class AreaTest extends JFrame{
private static final long serialVersionUID = -2221432546854106311L;
Area area = new Area();
ArrayList<Line2D.Double> areaSegments = new ArrayList<Line2D.Double>();
Point2D.Double insidePoint = new Point2D.Double(225, 225);
Point2D.Double closestPoint = new Point2D.Double(-1, -1);
Point2D.Double bestPoint = new Point2D.Double(-1, -1);
ArrayList<Point2D.Double> closestPointList = new ArrayList<Point2D.Double>();
AreaTest() {
Path2D.Double triangle = new Path2D.Double();
Random random = new Random();
// Draw three random triangles
for (int i = 0; i < 3; i++) {
triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
triangle.closePath();
area.add(new Area(triangle));
triangle.reset();
}
// Place a point inside the area
if (!area.contains(insidePoint)); {
while (!area.contains(insidePoint)) {
insidePoint.setLocation(random.nextInt(400) + 50, random.nextInt(400) + 50);
}
}
// Note: we're storing double[] and not Point2D.Double
ArrayList<double[]> areaPoints = new ArrayList<double[]>();
double[] coords = new double[6];
for (PathIterator pi = area.getPathIterator(null); !pi.isDone(); pi.next()) {
// Because the Area is composed of straight lines
int type = pi.currentSegment(coords);
// We record a double array of {segment type, x coord, y coord}
double[] pathIteratorCoords = {type, coords[0], coords[1]};
areaPoints.add(pathIteratorCoords);
}
double[] start = new double[3]; // To record where each polygon starts
for (int i = 0; i < areaPoints.size(); i++) {
// If we're not on the last point, return a line from this point to the next
double[] currentElement = areaPoints.get(i);
// We need a default value in case we've reached the end of the ArrayList
double[] nextElement = {-1, -1, -1};
if (i < areaPoints.size() - 1) {
nextElement = areaPoints.get(i + 1);
}
// Make the lines
if (currentElement[0] == PathIterator.SEG_MOVETO) {
start = currentElement; // Record where the polygon started to close it later
}
if (nextElement[0] == PathIterator.SEG_LINETO) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
nextElement[1], nextElement[2]
)
);
} else if (nextElement[0] == PathIterator.SEG_CLOSE) {
areaSegments.add(
new Line2D.Double(
currentElement[1], currentElement[2],
start[1], start[2]
)
);
}
}
// Calculate the nearest point on the edge
for (Line2D.Double line : areaSegments) {
// From: https://stackoverflow.com/questions/6176227
double u =
((insidePoint.getX() - line.x1) * (line.x2 - line.x1) + (insidePoint.getY() - line.y1) * (line.y2 - line.y1))
/ ((line.x2 - line.x1) * (line.x2 - line.x1) + (line.y2 - line.y1) * (line.y2 - line.y1));
double xu = line.x1 + u * (line.x2 - line.x1);
double yu = line.y1 + u * (line.y2 - line.y1);
if (u < 0) {
closestPoint.setLocation(line.getP1());
} else if (u > 1) {
closestPoint.setLocation(line.getP2());
} else {
closestPoint.setLocation(xu, yu);
}
closestPointList.add((Point2D.Double) closestPoint.clone());
if (closestPoint.distance(insidePoint) < bestPoint.distance(insidePoint)) {
bestPoint.setLocation(closestPoint);
}
}
setSize(new Dimension(500, 500));
setLocationRelativeTo(null); // To center the JFrame on screen
setDefaultCloseOperation(EXIT_ON_CLOSE);
setResizable(false);
setVisible(true);
}
public void paint(Graphics g) {
// Fill the area
Graphics2D g2d = (Graphics2D) g;
g.setColor(Color.lightGray);
g2d.fill(area);
// Draw the border line by line
g.setColor(Color.black);
for (Line2D.Double line : areaSegments) {
g2d.draw(line);
}
// Draw the inside point
g.setColor(Color.red);
g2d.fill(
new Ellipse2D.Double(
insidePoint.getX() - 3,
insidePoint.getY() - 3,
6,
6
)
);
// Draw the other close points
for (Point2D.Double point : closestPointList) {
g.setColor(Color.black);
g2d.fill(
new Ellipse2D.Double(
point.getX() - 3,
point.getY() - 3,
6,
6
)
);
}
// Draw the outside point
g.setColor(Color.green);
g2d.fill(
new Ellipse2D.Double(
bestPoint.getX() - 3,
bestPoint.getY() - 3,
6,
6
)
);
}
public static void main(String[] args) {
new AreaTest();
}
}
Here's the result:
And again: