Search code examples
java2dpointarea

Point Outside of Area Which is Closest to Point Inside?


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.


Solution

  • 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:

    enter image description here

    And again:

    enter image description here