多边形触碰检测 Google 地图 API V2

15

我正在尝试找出最佳方法来做这件事,我有一个地图,在地图上绘制了一个Polygon。由于Google Maps API V2似乎没有对多边形的触摸检测,所以我想知道是否可能检测触摸点是否在多边形内?如果可以的话,如何实现?我的主要目标是在地图上勾画出一个国家,在用户点击该国家时,将在自定义视图中显示更多详细信息。目前,我能够捕获地图的MapOnClick,但当用户点击多边形内部时,我希望在Toast上设置polygon.getID()。我是新手,所以如果不够清楚请见谅。

googleMap.setOnMapClickListener(new OnMapClickListener() 
    {
        public void onMapClick(LatLng point) 
        {
        boolean checkPoly = true;

        Toast.makeText(MainActivity.this,"The Location is outside of the Area", Toast.LENGTH_LONG).show();
        }    
     });
     }
     }
   catch (Exception e) {
         Log.e("APP","Failed", e);
     }    

好的,目前为止我已经半成功地完成了这个。

    private boolean rayCastIntersect(LatLng tap, LatLng vertA, LatLng vertB) {

    double aY = vertA.latitude;
    double bY = vertB.latitude;
    double aX = vertA.longitude;
    double bX = vertB.longitude;
    double pY = tap.latitude;
    double pX = tap.longitude;
     if (aY > bY) {
            aX = vertB.longitude;
            aY = vertB.latitude;
            bX = vertA.longitude;
            bX = vertA.latitude;
        }
    System.out.println("aY: "+aY+" aX : "+aX);
    System.out.println("bY: "+bY+" bX : "+bX);

     if (pX < 0) pX += 360;
        if (aX < 0) aX += 360;
        if (bX < 0) bX += 360;

        if (pY == aY || pY == bY) pY += 0.00000001;
        if ((pY > bY || pY < aY) || (pX > Math.max(aX, bX))) return false;
        if (pX < Math.min(aX, bX))

            return true;
//  }

    double m = (aX != bX) ? ((bY - aY) / (bX - aX)) : aX;
    double bee = (aX != pX) ? ((pY - aY) / (pX - aX)) : aX;
    double x = (pY - bee) / m;

    return x > pX;
}

我遇到的问题是在每个多边形的左侧触摸是正确的,直到它达到另一个多边形。我的算法有什么问题会导致这个问题?任何帮助都将不胜感激。


那么,为了澄清一下,您是想确定您的触摸是否发生在多边形的地理范围内吗? - Matt
7个回答

25
您要解决的问题是点在多边形内的测试,具体可参考点与多边形的位置关系
为了更好地理解射线法的概念:

请在纸上画一个多边形。从任意一点开始,向页面右侧画出一条直线。如果您的直线与多边形相交了奇数次,这意味着您的起始点在多边形内部。
那么,在代码中如何实现呢?
您的多边形由顶点列表构成:ArrayList<Geopoint> vertices。您需要逐个查看每个线段,并检查您的射线是否与其相交。
private boolean isPointInPolygon(Geopoint tap, ArrayList<Geopoint> vertices) {
    int intersectCount = 0;
    for(int j=0; j<vertices.size()-1; j++) {
        if( rayCastIntersect(tap, vertices.get(j), vertices.get(j+1)) ) {
            intersectCount++;
        }
    }

    return (intersectCount%2) == 1); // odd = inside, even = outside;
}

private boolean rayCastIntersect(Geopoint tap, Geopoint vertA, Geopoint vertB) {

    double aY = vertA.getLatitude();
    double bY = vertB.getLatitude();
    double aX = vertA.getLongitude();
    double bX = vertB.getLongitude();
    double pY = tap.getLatitude();
    double pX = tap.getLongitude();

    if ( (aY>pY && bY>pY) || (aY<pY && bY<pY) || (aX<pX && bX<pX) ) {
        return false; // a and b can't both be above or below pt.y, and a or b must be east of pt.x
    }

    double m = (aY-bY) / (aX-bX);               // Rise over run
    double bee = (-aX) * m + aY;                // y = mx + b
    double x = (pY - bee) / m;                  // algebra is neat!

    return x > pX;
}

好的,马特,我已经让它工作了,我的唯一问题是如果两个多边形在同一条x路径上,它会将它们视为一个,并使用相同的ID号。有什么想法吗? - Dwill
嗯,你能再详细解释一下吗?我的理解是如果两个多边形共享一个边,该应用程序会将它们视为一个多边形? - Matt
我不确定问题出在哪里。如果您的点击位于您正在测试的线段左侧,该函数应返回true。您需要在多边形中的每组相邻点上运行此函数,并收集此函数返回true的次数。 - Matt
如何处理“Polyline”? - Muhammad Babar
2
@MuhammadBabar 我建议你在另一个线程上提出这个问题。有几种方法可以解决这个问题。由于需要可点击区域缓冲区(用户恰好点击该行的可能性非常小),所以这很棘手。 - Matt
显示剩余3条评论

16

Google Maps Support 库现在有一个静态方法可以为您执行此检查:

PolyUtil.containsLocation(LatLng point, List<LatLng>polygon, boolean geodesic);

虽然文档在指南中没有明确提到该方法,但该方法确实存在。

Maps Support Library文档


这是更准确的方法。而且这个方法真的像魔法一样有效。感谢这个答案。应该相信这个方法,因为它来自于 Google Map 工具本身。 - Sagar Shah

10

通过Google Play服务8.4.0的发布,Maps API现在支持给多边形添加OnPolygonClickListener。多边形、折线和地图叠层都支持类似的事件,具体请参见多边形折线地图叠层

只需要调用GoogleMap.setOnPolygonClickListener(OnPolygonClickListener listener)即可设置,其他监听器也是基本相同的操作(如setOnPolylineClickListener等):

map.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() {  
    @Override  
    public void onPolygonClick(Polygon polygon) {  
        // Handle click ...  
    }  
});  

虽然有点晚,但这很好地解决了这种情况。


@Delta 是您在监听器中收到的多边形。 - matiash
1
@Delta7 你可以创建一个 Map<Polygon, Integer> 来存储引用,并将任何添加到地图中的多边形添加到其中。这样,您就可以查找相关联的 ID。 - matiash
你能从多边形中检索点击的坐标吗? - Gereltod
据我所知,不行。抱歉。 - matiash
如何获取与多边形相关的信息?假设有一些描述和多边形ID。 - NoWar
显示剩余3条评论

4
尽管user1504495已经简短地回答了我所使用的内容。但是,不要使用整个地图实用程序库,而是使用这些方法。
从您的活动类中相应地传递参数:
if (area.containsLocation(Touchablelatlong, listLatlong, true))
                isMarkerINSide = true;
            else
                isMarkerINSide = false;

把以下内容放在一个单独的类中:
/**
     * Computes whether the given point lies inside the specified polygon.
     * The polygon is always cosidered closed, regardless of whether the last point equals
     * the first or not.
     * Inside is defined as not containing the South Pole -- the South Pole is always outside.
     * The polygon is formed of great circle segments if geodesic is true, and of rhumb
     * (loxodromic) segments otherwise.
     */
    public static boolean containsLocation(LatLng point, List<LatLng> polygon, boolean geodesic) {
        final int size = polygon.size();
        if (size == 0) {
            return false;
        }
        double lat3 = toRadians(point.latitude);
        double lng3 = toRadians(point.longitude);
        LatLng prev = polygon.get(size - 1);
        double lat1 = toRadians(prev.latitude);
        double lng1 = toRadians(prev.longitude);
        int nIntersect = 0;
        for (LatLng point2 : polygon) {
            double dLng3 = wrap(lng3 - lng1, -PI, PI);
            // Special case: point equal to vertex is inside.
            if (lat3 == lat1 && dLng3 == 0) {
                return true;
            }
            double lat2 = toRadians(point2.latitude);
            double lng2 = toRadians(point2.longitude);
            // Offset longitudes by -lng1.
            if (intersects(lat1, lat2, wrap(lng2 - lng1, -PI, PI), lat3, dLng3, geodesic)) {
                ++nIntersect;
            }
            lat1 = lat2;
            lng1 = lng2;
        }
        return (nIntersect & 1) != 0;
    }

    /**
     * Wraps the given value into the inclusive-exclusive interval between min and max.
     * @param n   The value to wrap.
     * @param min The minimum.
     * @param max The maximum.
     */
    static double wrap(double n, double min, double max) {
        return (n >= min && n < max) ? n : (mod(n - min, max - min) + min);
    }

    /**
     * Returns the non-negative remainder of x / m.
     * @param x The operand.
     * @param m The modulus.
     */
    static double mod(double x, double m) {
        return ((x % m) + m) % m;
    }

    /**
     * Computes whether the vertical segment (lat3, lng3) to South Pole intersects the segment
     * (lat1, lng1) to (lat2, lng2).
     * Longitudes are offset by -lng1; the implicit lng1 becomes 0.
     */
    private static boolean intersects(double lat1, double lat2, double lng2,
                                      double lat3, double lng3, boolean geodesic) {
        // Both ends on the same side of lng3.
        if ((lng3 >= 0 && lng3 >= lng2) || (lng3 < 0 && lng3 < lng2)) {
            return false;
        }
        // Point is South Pole.
        if (lat3 <= -PI/2) {
            return false;
        }
        // Any segment end is a pole.
        if (lat1 <= -PI/2 || lat2 <= -PI/2 || lat1 >= PI/2 || lat2 >= PI/2) {
            return false;
        }
        if (lng2 <= -PI) {
            return false;
        }
        double linearLat = (lat1 * (lng2 - lng3) + lat2 * lng3) / lng2;
        // Northern hemisphere and point under lat-lng line.
        if (lat1 >= 0 && lat2 >= 0 && lat3 < linearLat) {
            return false;
        }
        // Southern hemisphere and point above lat-lng line.
        if (lat1 <= 0 && lat2 <= 0 && lat3 >= linearLat) {
            return true;
        }
        // North Pole.
        if (lat3 >= PI/2) {
            return true;
        }
        // Compare lat3 with latitude on the GC/Rhumb segment corresponding to lng3.
        // Compare through a strictly-increasing function (tan() or mercator()) as convenient.
        return geodesic ?
                tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3) :
                mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3);
    }

    /**
     * Returns tan(latitude-at-lng3) on the great circle (lat1, lng1) to (lat2, lng2). lng1==0.
     * See http://williams.best.vwh.net/avform.htm .
     */
    private static double tanLatGC(double lat1, double lat2, double lng2, double lng3) {
        return (tan(lat1) * sin(lng2 - lng3) + tan(lat2) * sin(lng3)) / sin(lng2);
    }

    /**
     * Returns mercator Y corresponding to latitude.
     * See http://en.wikipedia.org/wiki/Mercator_projection .
     */
    static double mercator(double lat) {
        return log(tan(lat * 0.5 + PI/4));
    }

    /**
     * Returns mercator(latitude-at-lng3) on the Rhumb line (lat1, lng1) to (lat2, lng2). lng1==0.
     */
    private static double mercatorLatRhumb(double lat1, double lat2, double lng2, double lng3) {
        return (mercator(lat1) * (lng2 - lng3) + mercator(lat2) * lng3) / lng2;
    } 

1
这个回答对我有效。不过,可以加入一个公差吗?也就是说,如果给定的位置在多边形外面 50 米左右,它仍然会将其验证为有效吗? - Emanuel

1
这里有一个完整的工作示例,用于判断触摸事件是否发生在多边形上。一些答案比必要的要复杂。这个解决方案使用了“android-maps-utils”库。
// compile 'com.google.maps.android:android-maps-utils:0.3.4'
private ArrayList<Polygon> polygonList = new ArrayList<>();

private void addMyPolygons() {
    PolygonOptions options = new PolygonOptions();
    // TODO: make your polygon's however you want
    Polygon polygon = googleMap.addPolygon(options);
    polygonList.add(polygon);
}

@Override
public void onMapClick(LatLng point) {
    boolean contains = false;
    for (Polygon p : polygonList) {
        contains = PolyUtil.containsLocation(point, p.getPoints(), false);
        if (contains) break;
    }
    Toast.makeText(getActivity(), "Click in polygon? "
            + contains, Toast.LENGTH_SHORT).show();
}

@Override
protected void onMapReady(View view, Bundle savedInstanceState) {
    googleMap.setOnMapClickListener(this);
    addMyPolygons();
}

0

我知道我发帖很晚了,但是我在这里发布的答案有些问题,所以我研究了前两个答案和一篇文章(我认为这是这种方法的起源),并修改了Matt Answer来编译出最适合我的东西。

Matt Answer的问题:它不计算多边形的最后一行(即由最后一个顶点和第一个顶点创建的行)

Dwill Answer的问题:当你已经对如何使事情工作感到沮丧时,它似乎很复杂和令人生畏

我添加的其他检查:

  • 检查是否实际创建了多边形
  • 检查多边形的任何一条边是否平行于y轴

我尽可能地进行了注释和解释,希望这对某些人有所帮助

还有一件事,这是用Dart编写的,主要集中在查找当前位置是否在地理围栏内。

Future<bool> checkIfLocationIsInsideBoundary({
  required LatLng positionToCheck,
  required List<LatLng> boundaryVertices,
}) async {

  // If there are less than 3 points then there will be no polygon
  if (boundaryVertices.length < 3) return false;

  int intersectCount = 0;
  // Check Ray-cast for lines created by all the vertices in our List
  for (int j = 0; j < boundaryVertices.length - 1; j++) {
    if (_rayCastIntersect(
      positionToCheck,
      boundaryVertices[j],
      boundaryVertices[j + 1],
    )) {
      intersectCount++;
    }
  }
  // Check for line created by the last vertex and the first vertex of the List
  if (_rayCastIntersect(
    positionToCheck,
    boundaryVertices.last,
    boundaryVertices.first,
  )) {
    intersectCount++;
  }

  // If our point is inside the polygon they will always intersect odd number of
  // times, else they will intersect even number of times
  return (intersectCount % 2) == 1; // odd = inside, even = outside
}

bool _rayCastIntersect(LatLng point, LatLng vertA, LatLng vertB) {
  final double aY = vertA.latitude;
  final double bY = vertB.latitude;
  final double aX = vertA.longitude;
  final double bX = vertB.longitude;
  final double pY = point.latitude;
  final double pX = point.longitude;

  // If vertices A and B are both above our point P then obviously the line made
  // by A and B cannot intersect with ray-cast of P. Note: Only y-coordinates of
  // each points can be used to check this.
  if (aY > pY && bY > pY) return false;

  // If vertices A and B are both below our point P then obviously the line made
  // by A and B cannot intersect with ray-cast of P. Note: Only y-coordinates of
  // each points can be used to check this.
  if (aY < pY && bY < pY) return false;

  // Since we will be casting ray on east side from our point P, at least one of
  // the vertex (either A or B) must be east of P else line made by A nd B
  // cannot intersect with ray-cast of P. Note: Only x-coordinates of each
  // points can be used to check this.
  if (aY < pY && bY < pY) return false;

  // If line made by vertices is parallel to Y-axis then we will get
  // 'Divided by zero` exception when calculating slope. In such case we can
  // only check if the line is on the east or the west relative to our point. If
  // it is on the east we count is as intersection. Note: we can be sure our
  // ray-cast will intersect the line because it is a vertical line, our
  // ray-cast is horizontal and finally we already made sure that both the
  // vertices are neither above nor below our point. Finally, since `aX == bX`
  // we can check if either aX or bX is on the right/east of pX
  if (aX == bX) return aX > pX;

  // Calculate slope of the line `m` made by vertices A and B using the formula
  // `m = (y2-y1) / (x2-x1)`
  final double m = (aY - bY) / (aX - bX); // Rise over run

  // Calculate the value of y-intersect `b` using the equation of line
  final double b = aY - (aX * m); // y = mx + b => b = y - mx

  // Now we translate our point P along X-axis such that it intersects our line.
  // This means we can pluck y-coordinate of our point P into the equation of
  // our line and calculate a new x-coordinate
  final double x = (pY - b) / m; // y = mx + b => x = (y - b) / m

  // Till now we have only calculated this new translated point but we don't
  // know if this point was translated towards west(left) of towards
  // east(right). This can be determined in the same way as we have done above,
  // if the x-coordinate of this new point is greater than x-coordinate of our
  // original point then it has shifted east, which means it has intersected our
  // line
  return x > pX;
}

-1

为了保持一致性 - 当用户点击多边形(或其他覆盖物)时,onMapClick不会被调用,并且在javadoc中有提到。

我做了一个解决方法,在MapFragment处理事件之前拦截点击事件,并将点投影到地图坐标上,并检查该点是否在任何多边形内,正如其他答案中建议的那样。

更多细节请参见此处


1
这有些不正确。onMapClick(LatLng point) 当用户在地图上进行轻拍手势时调用,但仅当地图的任何覆盖层都未处理该手势时才调用。。我找不到任何允许多边形或覆盖层处理此类事件的方法。因此似乎会调用 onMapClick() - dm78

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