Point Outside of Area Which is Closest to Point Inside?
Asked Answered
T

4

1

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.

Terrilyn answered 12/11, 2011 at 8:38 Comment(4)
Excluding the point it just came from?Fenestration
If that's the ONLY "safe" point, then the entity may stay there. But the entity should always move to the point closest to its preferred point.Terrilyn
Is it allowed to move to a safe point even if it is completely impossible to reach it because it is surrounded by forbidden area?Walkabout
The forbidden area is a "velocity obstacle." It would be impossible for the entity to "jump" over the forbidden area.Terrilyn
T
2

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

Terrilyn answered 16/11, 2011 at 17:57 Comment(0)
E
1

View my answer on this post

You can get the closest point outside of a polygon with a simple and lightweight approach:

Simply find the closest line segment, and find the perpendicular angle to that segment that intercepts the input point.

I don't need to describe it, just look at the image!


Example Code:

Vector2 is 2 doubles, x and y (Like Unity)

public class PolyCollisions {

    // Call this function...
    public static Vector2 doCollisions (Vector2[] polygon, Vector2 point) {

        if(!pointIsInPoly(polygon, point)) {
            // The point is not colliding with the polygon, so it does not need to change location
            return point;
        }

        // Get the closest point off the polygon
        return closestPointOutsidePolygon(polygon, point);

    }

    // Check if the given point is within the given polygon (Vertexes)
    // 
    // If so, call on collision if required, and move the point to the
    // closest point outside of the polygon
    public static boolean pointIsInPoly(Vector2[] verts, Vector2 p) {
        int nvert = verts.length;
        double[] vertx = new double[nvert];
        double[] verty = new double[nvert];
        for(int i = 0; i < nvert; i++) {
            Vector2 vert = verts[i];
            vertx[i] = vert.x;
            verty[i] = vert.y;
        }
        double testx = p.x;
        double testy = p.y;
        int i, j;
        boolean c = false;
        for (i = 0, j = nvert-1; i < nvert; j = i++) {
            if ( ((verty[i]>testy) != (verty[j]>testy)) &&
                    (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
                c = !c;
        }
        return c;
    }

    // Gets the closed point that isn't inside the polygon...
    public static Vector2 closestPointOutsidePolygon (Vector2[] poly, Vector2 point) {

        return getClosestPointInSegment(closestSegment(poly, point), point);

    }

    public static Vector2 getClosestPointInSegment (Vector2[] segment, Vector2 point) {

        return newPointFromCollision(segment[0], segment[1], point);

    }

    public static Vector2 newPointFromCollision (Vector2 aLine, Vector2 bLine, Vector2 p) {

        return nearestPointOnLine(aLine.x, aLine.y, bLine.x, bLine.y, p.x, p.y);

    }

    public static Vector2 nearestPointOnLine(double ax, double ay, double bx, double by, double px, double py) {

        // https://mcmap.net/q/1316865/-snap-point-to-a-line

        double apx = px - ax;
        double apy = py - ay;
        double abx = bx - ax;
        double aby = by - ay;

        double ab2 = abx * abx + aby * aby;
        double ap_ab = apx * abx + apy * aby;
        double t = ap_ab / ab2;
        if (t < 0) {
            t = 0;
        } else if (t > 1) {
            t = 1;
        }
        return new Vector2(ax + abx * t, ay + aby * t);
    }

    public static Vector2[] closestSegment (Vector2[] points, Vector2 point) {

        Vector2[] returns = new Vector2[2];

        int index = closestPointIndex(points, point);

        returns[0] = points[index];

        Vector2[] neighbors = new Vector2[] {
                points[(index+1+points.length)%points.length],
                points[(index-1+points.length)%points.length]
        };

        double[] neighborAngles = new double[] {
                getAngle(new Vector2[] {point, returns[0], neighbors[0]}),
                getAngle(new Vector2[] {point, returns[0], neighbors[1]})
        };

        if(neighborAngles[0] < neighborAngles[1]) {
            returns[1] = neighbors[0];
        } else {
            returns[1] = neighbors[0];
        }

        return returns;

    }

    public static double getAngle (Vector2[] abc) {

        // https://mcmap.net/q/100341/-how-to-calculate-an-angle-from-three-points-closed
        // atan2(P2.y - P1.y, P2.x - P1.x) - atan2(P3.y - P1.y, P3.x - P1.x)
        return Math.atan2(abc[2].y - abc[0].y, abc[2].x - abc[0].x) - Math.atan2(abc[1].y - abc[0].y, abc[1].x - abc[0].x);

    }

    //public static Vector2 lerp (Vector2 a, Vector2 b, double c) {
    //  
    //  return new Vector2(c*(a.x-b.x)+b.x, c*(a.y-b.y)+b.y);
    //  
    //}

    /*public static Vector2 closestPoint (Vector2[] points, Vector2 point) {

        int leastDistanceIndex = 0;
        double leastDistance = Double.MAX_VALUE;

        for(int i = 0; i < points.length; i++) {
            double dist = distance(points[i], point);
            if(dist < leastDistance) {
                leastDistanceIndex = i;
                leastDistance = dist;
            }
        }

        return points[leastDistanceIndex];

    }*/

    public static int closestPointIndex (Vector2[] points, Vector2 point) {

        int leastDistanceIndex = 0;
        double leastDistance = Double.MAX_VALUE;

        for(int i = 0; i < points.length; i++) {
            double dist = distance(points[i], point);
            if(dist < leastDistance) {
                leastDistanceIndex = i;
                leastDistance = dist;
            }
        }

        return leastDistanceIndex;

    }

    public static double distance (Vector2 a, Vector2 b) {
        return Math.sqrt(Math.pow(Math.abs(a.x-b.x), 2)+Math.pow(Math.abs(a.y-b.y), 2));
    }

}

Useful Links / Answers

Snap Point to Line

How to calculate an angle from 3 points

Elyot answered 15/7, 2017 at 23:58 Comment(0)
A
0

The most easy (and most inefficient) approach would be a brute force. You have a preferred point inside an area. to find the closest point to it: hold two variables, one for minimal distance and one for current closest point. now simply step over every other point in your two dimensional space: if that point is not inside the forbidden area (or any forbidden area if there are many), then calculate the distance between it and the preferred point. If that distance is less than the current minimal distance, then make it become the current minimal distance and make the point become the current closest point. when you finish, you will have the closest point outside the area and if none was found, you stay on your original point.

I am not specialist in geometry algorithms, but if the two dimensional space is very big and the calculation is not finishing fast enough, maybe you can try to improve it with the following: the Area class has a contains method that "tests if the interior of the Shape entirely contains the specified rectangular area". therefore, start creating rectangles(or squares) around the preferred point. you start with the minimal rectangle surrounding the point and on every loop you increase it by one point in each direction. for every rectangle that you create, check if it is contained in the area. you stop calculating rectangles when you hit the first rectangle that is not entirely contained in the area. then, you use the above algorithm (the brute force) but only on points contained in this rectangle and that are not inside the area.

Atomic answered 12/11, 2011 at 12:42 Comment(0)
F
0

The formula for distance between two points is (javascript):

var xDiff = ( point1x - point2x ),
    yDiff = ( point1y - point2y ),
    distance = Math.sqrt( ( xDiff * xDiff ) + ( yDiff * yDiff ) );

Loop around your "proposed new point", starting at one x-1, y-1 to x+1, y+1. At each point check to see that it's not a forbidden point, not the point you just came from, and not off the boundaries of the map. If it meets all those criteria, use the above formula to measure the distance and add it to an array. At the end of your "1-point out" loop, check if there are any distances in that array. If so, take the smallest one and you're done. If there aren't any, move onto x-2, y-2 to x+2, y+2 (2 points out).

This will be extremely fast for the small area you are referring to.

Demo: http://jsfiddle.net/ThinkingStiff/V7Bqm/

var X = 0, 
    Y = 1,
    currentPoint = [5,5],
    proposedPoint = [5,6],
    forbiddenPoints = [[5,6],[6,6],[4,7],[5,7],[6,7],[4,8],[5,8]],
    map = { left:1, top:1, right:10, bottom:10 };

function closestSafePoint( point ) {

    var x = point[X], y = point[Y], safePoints = [];

    for( var left = x - 1, top = y - 1, right = x + 1, bottom = y + 1;
         left <= map.left || top <= map.top || right <= map.right || bottom <= map.bottom;
         left--, top--, right++, bottom++) {

        checkHorizontalPoints( safePoints, point, left, right, top );
        checkHorizontalPoints( safePoints, point, left, right, bottom );
        checkVerticalPoints( safePoints, point, top + 1, bottom - 1, left );
        checkVerticalPoints( safePoints, point, top + 1, bottom - 1, right );

        safePoints.sort( function( a, b ){ return a[1] - b[1] } );

        return safePoints.length ? safePoints[0] : point;

    };

};

function checkHorizontalPoints( points, fromPoint, startX, endX, y ) {

    for( var x = startX; x <= endX ; x++ ) {

        var toPoint = [x, y];

        if( !isForbidden( toPoint ) && !isCurrent( toPoint) && onMap( toPoint ) ) {

            points.push( [toPoint, distance( fromPoint, toPoint )] );

        };

    };

};

function checkVerticalPoints( points, fromPoint, startY, endY, x ) {

    for( var y = startY; y <= endY ; y++ ) {

        var toPoint = [x, y];

        if( !isForbidden( toPoint ) && !isCurrent( toPoint) && onMap( toPoint ) ) {

            points.push( [toPoint, distance( fromPoint, toPoint )] );

        };

    };

};

function isForbidden( point ) {

    for( var index = 0; index < forbiddenPoints.length; index++ ) {

        if( forbiddenPoints[index].toString() == point.toString() ) return true;

    };

};

function isCurrent( point ) {

    return currentPoint.toString() == point.toString() ? true : false;

};

function onMap( point ) {

    var x = point[X], y = point[Y];
    return x >= map.left && y >= map.top && x <= map.right && y <= map.bottom;

};

function distance( pointA, pointB ) {

    var xDiff = ( pointA[X] - pointB[X] ),
        yDiff = ( pointA[Y] - pointB[Y] );

    return Math.sqrt( ( xDiff * xDiff ) + ( yDiff * yDiff ) );    

};

console.log( 
      'current: ' + currentPoint + ', '
    + 'proposed: ' + proposedPoint + ', '
    + 'closest: ' + closestSafePoint( proposedPoint )[0] 
);

One optimization you could make to this, if you're fairly sure most of your safe spots will be one or two points away is to break out as soon as you get to a point thats distance is the same as the level you're on. So if you're on loop one, and you get a point that is distance = 1, stop, since you'll never get closer than that.

UPDATE: I noticed you added "same trajectory" to your question. But in one of the comments, you also say it can't jump over the forbidden area. Those statements seem to conflict.

Same trajectory is a little more tricky and requires some trig. Check out my demo of circular divs at http://jsfiddle.net/ThinkingStiff/uLu7v/. There is a "point on ray" function halfway down at:

$this.siblings( ".circle" ).each( function()

This calculates the distance to move the surrounding circles on a ray away from the selected circle. This could be used to calculate a point on your trajectory. But, I think my original function is actually what you're looking for and you didn't mean same trajectory.

Fenestration answered 12/11, 2011 at 17:28 Comment(3)
It should be noted that I'm employing this in an agent-based simulation, so several thousand autonomous entities need to perform this calculation every single timestep.Terrilyn
@Terrilyn I added some code you can play with. It might work for what you're doing.Fenestration
From a performance point-of-view, I'd skip squaring the result and do: distance2 = xDiff * xDiff + yDiff * yDiff. Otherwise, for using Pythagoras, there is: java.lang.Math.hypot(double, double).Dubitable

© 2022 - 2024 — McMap. All rights reserved.