在Android中查找包含在路径中的点

10
他们决定在Android中不添加Path.contains方法(用于路径),这是否有原因?
我想知道我在一个路径中有哪些点,并希望它比在此处看到的更容易: 如何判断封闭路径是否包含给定点? 对我来说,创建ArrayList并将整数添加到数组中是否更好? (我只在控制语句中检查一次点)即if(myPath.contains(x,y) 到目前为止,我的选项是:
  • 使用Region
  • 使用ArrayList
  • 扩展类
  • 你的建议
我只是寻找最有效的方法。

可能是重复的问题,参考如何判断一个封闭路径是否包含给定点? - Trilarion
4个回答

14

我前不久也遇到了同样的问题,在搜索后,我发现这是最好的解决方案。

Java有一个带有contains()方法的Polygon类,可以使事情变得非常简单。不幸的是,java.awt.Polygon类在Android上不受支持。但是,我找到了一个人编写的等效类

我不认为您可以从AndroidPath类中获取构成路径的单个点,因此您将不得不以不同的方式存储数据。

该类使用交叉数算法来确定给定点是否在给定点列表内部。

/**
 * 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;
    }  
}

如果不确定多边形有多少边,该怎么办? - StartingGroovy
边数可以根据点的数量轻松推断。矩形有4个点和4条边。三角形有3个点和3条边。由连续路径绘制的多边形具有相同数量的边和点,因此如果您的路径有“n”个点,则它也有“n”条边。 - theisenp
太棒了,我很高兴能帮忙。这个解决方案几周前也曾经帮我省去了大量麻烦。 - theisenp
是的,他们没有包含contains方法确实有点奇怪。不过,我很高兴你用这个类节省了我大量的时间。干杯! - StartingGroovy
这非常有帮助。如果您感兴趣,我在这里找到了关于交叉数和绕组数算法的信息(我觉得非常有启发性):http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm - snapfractalpop
...你的回答也为我节省了很多时间。谢谢,theisenp! - Florin Mircea

7
尝试了其他答案,但对于我的情况给出了错误的结果。没有找到确切的原因,但我从以下算法进行了自己的直接翻译:http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
现在代码如下:
/**
 * 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;
    }  
}

谢谢,伙计。你帮我省了很多麻烦。我一开始实现了@theisenp的答案,但效果不尽如人意。给你点赞。 - Vikrant_Dev
路径带有弧线怎么办?算法使用顶点数组。似乎没有考虑路径包含弧线或更一般的圆形路径的情况。 - kinghomer
非常感谢您发布这个。 - Edward Falk

7
我想评论@theisenp的回答:代码中有整数数组,如果您查看算法描述网页,它会警告不要使用整数而应该使用浮点数。
我复制了您上面的代码,除了一些不太好连接的边角情况外,它似乎运行良好。
通过将所有内容更改为浮点数,我解决了这个问题。

谢谢伙计。我也遇到了同样的准确性问题,看了你的评论后解决了,感谢。你得到了+1 :) - samir

2

为了完整起见,在这里我想做几个注释:

从API 19开始,路径有一个交集操作。您可以在测试点周围创建一个非常小的正方形路径,将其与路径相交,并查看结果是否为空。

您可以将路径转换为区域并执行包含()操作。但是,区域使用整数坐标,并且我认为它们使用变换后的(像素)坐标,因此您必须处理这些内容。我还怀疑转换过程计算密集。

Hans发布的边界穿越算法很好且快速,但是您必须非常小心,以避免某些角落情况,例如光线直接通过顶点、相交水平边缘或舍入误差成为问题时,这总是存在的。

绕数方法非常可靠,但涉及大量三角函数计算,计算成本高。

Dan Sunday的这篇论文提供了一种混合算法,与绕数一样准确,但计算简单,类似于射线投射算法。它的优雅之处让我惊叹不已。

我的代码

这是我最近用Java编写的一些代码,用于处理由线段圆弧组成的路径。(还有圆,但那些是自成一体的完整路径,所以这是一种退化情况。)

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;
        }
    }
}

编辑:按照要求,添加一些使用此功能的示例代码。
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();
    }   

请问您能否提供一个使用您的库的示例,包括arc的例子?我感觉自己做错了什么。实际上,我不确定x0、y0、x1、y1应该发送什么。这是来自RectF对象的顶部、底部、左侧和右侧点吗?(我是Android新手) - ShP
是的,Android通过指定椭圆的边界,然后指定起始和结束角度来指定弧线。我在http://www.efalk.org/Docs/Android/graphics_0.html#arcTo上有一些注释。 - Edward Falk
你能举个使用它的例子吗?@ShP 你明白了吗? - Bhaven Shah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接