Finding Points contained in a Path in Android

前端 未结 4 2312
情深已故
情深已故 2020-12-13 02:51

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 t

相关标签:
4条回答
  • 2020-12-13 03:35

    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;
        }  
    }
    
    0 讨论(0)
  • 2020-12-13 03:43

    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();
        }   
    
    0 讨论(0)
  • 2020-12-13 03:48

    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;
        }  
    }
    
    0 讨论(0)
  • 2020-12-13 03:52

    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.

    0 讨论(0)
提交回复
热议问题