Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding Points contained in a Path in Android

Is there a reason that they decided not to add the contains method (for Path) in Android?

I'm wanting to know what points I have in a Path and hoped it was easier than seen here:

How can I tell if a closed path contains a given point?

Would it be better for me to create an ArrayList and add the integers into the array? (I only check the points once in a control statement) Ie. if(myPath.contains(x,y)

So far my options are:

  • Using a Region
  • Using an ArrayList
  • Extending the Class
  • Your suggestion

I'm just looking for the most efficient way I should go about this

like image 273
StartingGroovy Avatar asked Aug 12 '11 18:08

StartingGroovy


4 Answers

For completeness, I want to make a couple notes here:

As of API 19, there is an intersection operation for Paths. You could create a very small square path around your test point, intersect it with the Path, and see if the result is empty or not.

You can convert Paths to Regions and do a contains() operation. However Regions work in integer coordinates, and I think they use transformed (pixel) coordinates, so you'll have to work with that. I also suspect that the conversion process is computationally intensive.

The edge-crossing algorithm that Hans posted is good and quick, but you have to be very careful for certain corner cases such as when the ray passes directly through a vertex, or intersects a horizontal edge, or when round-off error is a problem, which it always is.

The winding number method is pretty much fool proof, but involves a lot of trig and is computationally expensive.

This paper by Dan Sunday gives a hybrid algorithm that's as accurate as the winding number but as computationally simple as the ray-casting algorithm. It blew me away how elegant it was.

My code

This is some code I wrote recently in Java which handles a path made out of both line segments and arcs. (Also circles, but those are complete paths on their own, so it's sort of a degenerate case.)

package org.efalk.util;

/**
 * Utility: determine if a point is inside a path.
 */
public class PathUtil {
    static final double RAD = (Math.PI/180.);
    static final double DEG = (180./Math.PI);

    protected static final int LINE = 0;
    protected static final int ARC = 1;
    protected static final int CIRCLE = 2;

    /**
     * Used to cache the contents of a path for pick testing.  For a
     * line segment, x0,y0,x1,y1 are the endpoints of the line.  For
     * a circle (ellipse, actually), x0,y0,x1,y1 are the bounding box
     * of the circle (this is how Android and X11 like to represent
     * circles).  For an arc, x0,y0,x1,y1 are the bounding box, a1 is
     * the start angle (degrees CCW from the +X direction) and a1 is
     * the sweep angle (degrees CCW).
     */
    public static class PathElement {
        public int type;
        public float x0,y0,x1,y1;   // Endpoints or bounding box
        public float a0,a1;         // Arcs and circles
    }

    /**
     * Determine if the given point is inside the given path.
     */
    public static boolean inside(float x, float y, PathElement[] path) {
        // Based on algorithm by Dan Sunday, but allows for arc segments too.         
        // http://geomalgorithms.com/a03-_inclusion.html
        int wn = 0;
        // loop through all edges of the polygon
        // An upward crossing requires y0 <= y and y1 > y
        // A downward crossing requires y0 > y and y1 <= y
        for (PathElement pe : path) {
            switch (pe.type) {
              case LINE:
                if (pe.x0 < x && pe.x1 < x) // left
                    break;
                if (pe.y0 <= y) {           // start y <= P.y
                    if (pe.y1 > y) {        // an upward crossing
                        if (isLeft(pe, x, y) > 0) // P left of  edge
                            ++wn;                // have  a valid up intersect
                    }
                }
                else {                              // start y > P.y
                    if (pe.y1 <= y) {       // a downward crossing
                        if (isLeft(pe, x, y) < 0) // P right of  edge
                            --wn;                // have  a valid down intersect
                    }
                }
                break;
              case ARC:
                wn += arcCrossing(pe, x, y);
                break;
              case CIRCLE:
                // This should be the only element in the path, so test it
                // and get out.
                float rx = (pe.x1-pe.x0)/2;
                float ry = (pe.y1-pe.y0)/2;
                float xc = (pe.x1+pe.x0)/2;
                float yc = (pe.y1+pe.y0)/2;
                return (x-xc)*(x-xc)/rx*rx + (y-yc)*(y-yc)/ry*ry <= 1;
            }
        }
        return wn != 0;
    }

    /**
     * Return >0 if p is left of line p0-p1; <0 if to the right; 0 if
     * on the line.
     */
    private static float
    isLeft(float x0, float y0, float x1, float y1, float x, float y)
    {
        return (x1 - x0) * (y - y0) - (x - x0) * (y1 - y0);
    }

    private static float isLeft(PathElement pe, float x, float y) {
        return isLeft(pe.x0,pe.y0, pe.x1,pe.y1, x,y);
    }

    /**
     * Determine if an arc segment crosses the test ray up or down, or not
     * at all.
     * @return winding number increment:
     *      +1 upward crossing
     *       0 no crossing
     *      -1 downward crossing
     */
    private static int arcCrossing(PathElement pe, float x, float y) {
        // Look for trivial reject cases first.
        if (pe.x1 < x || pe.y1 < y || pe.y0 > y) return 0;

        // Find the intersection of the test ray with the arc. This consists
        // of finding the intersection(s) of the line with the ellipse that
        // contains the arc, then determining if the intersection(s)
        // are within the limits of the arc.
        // Since we're mostly concerned with whether or not there *is* an
        // intersection, we have several opportunities to punt.
        // An upward crossing requires y0 <= y and y1 > y
        // A downward crossing requires y0 > y and y1 <= y
        float rx = (pe.x1-pe.x0)/2;
        float ry = (pe.y1-pe.y0)/2;
        float xc = (pe.x1+pe.x0)/2;
        float yc = (pe.y1+pe.y0)/2;
        if (rx == 0 || ry == 0) return 0;
        if (rx < 0) rx = -rx;
        if (ry < 0) ry = -ry;
        // We start by transforming everything so the ellipse is the unit
        // circle; this simplifies the math.
        x -= xc;
        y -= yc;
        if (x > rx || y > ry || y < -ry) return 0;
        x /= rx;
        y /= ry;
        // Now find the points of intersection. This is simplified by the
        // fact that our line is horizontal. Also, by the time we get here,
        // we know there *is* an intersection.
        // The equation for the circle is x²+y² = 1. We have y, so solve
        // for x = ±sqrt(1 - y²)
        double x0 = 1 - y*y;
        if (x0 <= 0) return 0;
        x0 = Math.sqrt(x0);
        // We only care about intersections to the right of x, so
        // that's another opportunity to punt. For a CCW arc, The right
        // intersection is an upward crossing and the left intersection
        // is a downward crossing.  The reverse is true for a CW arc.
        if (x > x0) return 0;
        int wn = arcXing1(x0,y, pe.a0, pe.a1);
        if (x < -x0) wn -= arcXing1(-x0,y, pe.a0, pe.a1);
        return wn;
    }

    /**
     * Return the winding number of the point x,y on the unit circle
     * which passes through the arc segment defined by a0,a1.
     */
    private static int arcXing1(double x, float y, float a0, float a1) {
        double a = Math.atan2(y,x) * DEG;
        if (a < 0) a += 360;
        if (a1 > 0) {       // CCW
            if (a < a0) a += 360;
            return a0 + a1 > a ? 1 : 0;
        } else {            // CW
            if (a0 < a) a0 += 360;
            return a0 + a1 <= a ? -1 : 0;
        }
    }
}

Edit: by request, adding some sample code that makes use of this.

import PathUtil;
import PathUtil.PathElement;

/**
 * This class represents a single geographic area defined by a
 * circle or a list of line segments and arcs.
 */
public class Area {
    public float lat0, lon0, lat1, lon1;    // bounds
    Path path = null;
    PathElement[] pathList;

    /**
     * Return true if this point is inside the area bounds. This is
     * used to confirm touch events and may be computationally expensive.
     */
    public boolean pointInBounds(float lat, float lon) {
        if (lat < lat0 || lat > lat1 || lon < lon0 || lon > lon1)
            return false;
        return PathUtil.inside(lon, lat, pathList);
    }

    static void loadBounds() {
        int n = number_of_elements_in_input;
        path = new Path();
        pathList = new PathElement[n];
        for (Element element : elements_in_input) {
            PathElement pe = new PathElement();
            pathList[i] = pe;
            pe.type = element.type;
            switch (element.type) {
              case LINE:        // Line segment
                pe.x0 = element.x0;
                pe.y0 = element.y0;
                pe.x1 = element.x1;
                pe.y1 = element.y1;
                // Add to path, not shown here
                break;
              case ARC: // Arc segment
                pe.x0 = element.xmin;     // Bounds of arc ellipse
                pe.y0 = element.ymin;
                pe.x1 = element.xmax;
                pe.y1 = element.ymax;
                pe.a0 = a0; pe.a1 = a1;
                break;
              case CIRCLE: // Circle; hopefully the only entry here
                pe.x0 = element.xmin;    // Bounds of ellipse
                pe.y0 = element.ymin;
                pe.x1 = element.xmax;
                pe.y1 = element.ymax;
                // Add to path, not shown here
                break;
            }
        }
        path.close();
    }   
like image 61
Edward Falk Avatar answered Nov 15 '22 08:11

Edward Falk


Tried the other answer, but it gave an erroneous outcome for my case. Didn't bother to find the exact cause, but made my own direct translation from the algorithm on: http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html

Now the code reads:

/**
 * Minimum Polygon class for Android.
 */
public class Polygon
{
    // Polygon coodinates.
    private int[] polyY, polyX;

    // Number of sides in the polygon.
    private int polySides;

    /**
     * Default constructor.
     * @param px Polygon y coods.
     * @param py Polygon x coods.
     * @param ps Polygon sides count.
     */
    public Polygon( int[] px, int[] py, int ps )
    {
        polyX = px;
        polyY = py;
        polySides = ps;
    }

    /**
     * Checks if the Polygon contains a point.
     * @see "http://alienryderflex.com/polygon/"
     * @param x Point horizontal pos.
     * @param y Point vertical pos.
     * @return Point is in Poly flag.
     */
    public boolean contains( int x, int y )
    {
        boolean c = false;
        int i, j = 0;
        for (i = 0, j = polySides - 1; i < polySides; j = i++) {
            if (((polyY[i] > y) != (polyY[j] > y))
                && (x < (polyX[j] - polyX[i]) * (y - polyY[i]) / (polyY[j] - polyY[i]) + polyX[i]))
            c = !c;
        }
        return c;
    }  
}
like image 27
Hans Avatar answered Nov 15 '22 08:11

Hans


I came up against this same problem a little while ago, and after some searching, I found this to be the best solution.

Java has a Polygon class with a contains() method that would make things really simple. Unfortunately, the java.awt.Polygonclass is not supported in Android. However, I was able to find someone who wrote an equivalent class.

I don't think you can get the individual points that make up the path from the Android Path class, so you will have to store the data in a different way.

The class uses a Crossing Number algorithm to determine whether or not the point is inside of the given list of points.

/**
 * Minimum Polygon class for Android.
 */
public class Polygon
{
    // Polygon coodinates.
    private int[] polyY, polyX;

    // Number of sides in the polygon.
    private int polySides;

    /**
     * Default constructor.
     * @param px Polygon y coods.
     * @param py Polygon x coods.
     * @param ps Polygon sides count.
     */
    public Polygon( int[] px, int[] py, int ps )
    {
        polyX = px;
        polyY = py;
        polySides = ps;
    }

    /**
     * Checks if the Polygon contains a point.
     * @see "http://alienryderflex.com/polygon/"
     * @param x Point horizontal pos.
     * @param y Point vertical pos.
     * @return Point is in Poly flag.
     */
    public boolean contains( int x, int y )
    {
        boolean oddTransitions = false;
        for( int i = 0, j = polySides -1; i < polySides; j = i++ )
        {
            if( ( polyY[ i ] < y && polyY[ j ] >= y ) || ( polyY[ j ] < y && polyY[ i ] >= y ) )
            {
                if( polyX[ i ] + ( y - polyY[ i ] ) / ( polyY[ j ] - polyY[ i ] ) * ( polyX[ j ] - polyX[ i ] ) < x )
                {
                    oddTransitions = !oddTransitions;          
                }
            }
        }
        return oddTransitions;
    }  
}
like image 35
theisenp Avatar answered Nov 15 '22 07:11

theisenp


I would just like to comment on @theisenp answer: The code has integer arrays and if you look on the algorithm description webpage it warns against using integers instead of floating point.

I copied your code above and it seemed to work fine except for some corner cases when I made lines that didnt connect to themselves very well.

By changing everything to floating point, I got rid of this bug.

like image 7
Sebastian Avatar answered Nov 15 '22 07:11

Sebastian