判断点是否在任意多边形内
最近项目用到:在Google map上判断事发地点,是否在管辖区域内。也就是典型的判断一个点是否在不规则任意多边形内的例子。
但是Google Map没有提供相应的api,找资料发现百度地图提供了一个工具类,肿么办,为了一个工具类,加入百度地图吗,操蛋,这是不可能的!
百度地图api链接:http://wiki.lbsyun.baidu.com/cms/androidsdk/doc/v3_7_0/com/baidu/mapapi/utils/SpatialRelationUtil.html
Point Inclusion
• 给定一个点和一个不规则多边形,如果判断点在多边形内部还是外部?
• 方向有助于在线性时间解决这个问题!
Point Inclusion — Part II
• 在每个点的右侧绘制一条水平线,并延伸到无穷远。(水平射线)
• 计算水平射线与多边形相交的次数。
我们有结论:
- 偶数 ⇒ 点在外部
- 奇数 ⇒ 点在内部
• d 和 g点怎么办? Degeneracy! (在判断一个点的水平射线和多边形一个边是否相交时,依据是:点的竖向坐标y 是否在 线段的竖向坐标(Ymin,Ymax]范围内,而d g点是y值完全等于多边形某一点的y值。而多边形的一个点必然关联两条线段。所以在判断空间关系时,无论取开区间(Ymin,Ymax) 还是闭区间[Ymin,Ymax], 必然造成双重计数,不影响结论)
具体演示效果见
GitHub:https://github.com/shaoshuai904/GoogleMap_Demo
下面是代码部分:
/** * Polygon 与 Point 空间关系 工具类 * * @author maple */ public class SpatialRelationUtil { /** * 返回一个点是否在一个多边形区域内(推荐) * * @param mPoints 多边形坐标点列表 * @param point 待判断点 * @return true 多边形包含这个点,false 多边形未包含这个点。 */ public static boolean isPolygonContainsPoint1(List<LatLng> mPoints, LatLng point) { LatLngBounds.Builder boundsBuilder = LatLngBounds.builder(); for (LatLng ll : mPoints) boundsBuilder.include(ll); // 如果point不在多边形Bounds范围内,直接返回false。 if (boundsBuilder.build().contains(point)) { return isPolygonContainsPoint(mPoints, point); } else { return false; } } /** * 返回一个点是否在一个多边形区域内 * * @param mPoints 多边形坐标点列表 * @param point 待判断点 * @return true 多边形包含这个点,false 多边形未包含这个点。 */ public static boolean isPolygonContainsPoint(List<LatLng> mPoints, LatLng point) { int nCross = 0; for (int i = 0; i < mPoints.size(); i++) { LatLng p1 = mPoints.get(i); LatLng p2 = mPoints.get((i + 1) % mPoints.size()); // 取多边形任意一个边,做点point的水平延长线,求解与当前边的交点个数 // p1p2是水平线段,要么没有交点,要么有无限个交点 if (p1.longitude == p2.longitude) continue; // point 在p1p2 底部 --> 无交点 if (point.longitude < Math.min(p1.longitude, p2.longitude)) continue; // point 在p1p2 顶部 --> 无交点 if (point.longitude >= Math.max(p1.longitude, p2.longitude)) continue; // 求解 point点水平线与当前p1p2边的交点的 X 坐标 double x = (point.longitude - p1.longitude) * (p2.latitude - p1.latitude) / (p2.longitude - p1.longitude) + p1.latitude; if (x > point.latitude) // 当x=point.x时,说明point在p1p2线段上 nCross++; // 只统计单边交点 } // 单边交点为偶数,点在多边形之外 --- return (nCross % 2 == 1); } /** * 返回一个点是否在一个多边形边界上 * * @param mPoints 多边形坐标点列表 * @param point 待判断点 * @return true 点在多边形边上,false 点不在多边形边上。 */ public static boolean isPointInPolygonBoundary(List<LatLng> mPoints, LatLng point) { for (int i = 0; i < mPoints.size(); i++) { LatLng p1 = mPoints.get(i); LatLng p2 = mPoints.get((i + 1) % mPoints.size()); // 取多边形任意一个边,做点point的水平延长线,求解与当前边的交点个数 // point 在p1p2 底部 --> 无交点 if (point.longitude < Math.min(p1.longitude, p2.longitude)) continue; // point 在p1p2 顶部 --> 无交点 if (point.longitude > Math.max(p1.longitude, p2.longitude)) continue; // p1p2是水平线段,要么没有交点,要么有无限个交点 if (p1.longitude == p2.longitude) { double minX = Math.min(p1.latitude, p2.latitude); double maxX = Math.max(p1.latitude, p2.latitude); // point在水平线段p1p2上,直接return true if ((point.longitude == p1.longitude) && (point.latitude >= minX && point.latitude <= maxX)) { return true; } } else { // 求解交点 double x = (point.longitude - p1.longitude) * (p2.latitude - p1.latitude) / (p2.longitude - p1.longitude) + p1.latitude; if (x == point.latitude) // 当x=point.x时,说明point在p1p2线段上 return true; } } return false; } }
使用说明:只需要将SpatialRelationUtil这个工具类,复制到你的项目就可以直接使用,不用添加任何jar包。
好多人说不知道LatLngBounds类的具体实现,其实这是Google map包中的一个类,内部功能很简单,就是提供了一个构造器Builder可以不断的往里面添加经纬度点LatLng,不断计算更新Bounds范围和center中心点。
下面贴上整理后的LatLngBounds类,可以减去导入Google map包的麻烦
/** * 经纬度范围类 * * 复写com.google.android.gms.maps.model.LatLngBounds中核心方法 * * @author maple */ class LatLngBounds constructor( private val southwest: LatLng,// 左下角 点 private val northeast: LatLng // 右上角 点 ) { val center: LatLng get() { // 计算中心点纬度 val centerLat = (this.southwest.latitude + this.northeast.latitude) / 2.0 // 计算中心点经度 val neLng = this.northeast.longitude // 右上角 经度 val swLng: Double = this.southwest.longitude // 左下角 经度 val centerLng: Double = if (swLng <= neLng) { (neLng + swLng) / 2.0 } else { (neLng + 360.0 + swLng) / 2.0 } return LatLng(centerLat, centerLng) } // 某个点是否在该范围内(包含边界) fun contains(point: LatLng): Boolean { return latContains(point.latitude) && this.lngContains(point.longitude) } // 某个纬度值是否在该范围内(包含边界) private fun latContains(lat: Double): Boolean { return this.southwest.latitude <= lat && lat <= this.northeast.latitude } // 某个经度值是否在该范围内(包含边界) private fun lngContains(lng: Double): Boolean { return if (this.southwest.longitude <= this.northeast.longitude) { this.southwest.longitude <= lng && lng <= this.northeast.longitude } else { this.southwest.longitude <= lng || lng <= this.northeast.longitude } } // 小数据量可以使用该方法,大数据量建议使用Builder中的include() fun including(point: LatLng): LatLngBounds { val swLat = Math.min(this.southwest.latitude, point.latitude) val neLat = Math.max(this.northeast.latitude, point.latitude) var neLng = this.northeast.longitude var swLng = this.southwest.longitude val pLng = point.longitude if (!this.lngContains(pLng)) { if (zza(swLng, pLng) < zzb(neLng, pLng)) { swLng = pLng } else { neLng = pLng } } return LatLngBounds(LatLng(swLat, swLng), LatLng(neLat, neLng)) } /** * LatLngBounds生成器 */ class Builder { private var swLat = 1.0 / 0.0 // 左下角 纬度 private var swLng = 0.0 / 0.0 // 左下角 经度 private var neLat = -1.0 / 0.0 // 右上角 纬度 private var neLng = 0.0 / 0.0 // 右上角 经度 fun include(point: LatLng): Builder { this.swLat = Math.min(this.swLat, point.latitude) this.neLat = Math.max(this.neLat, point.latitude) val pLng = point.longitude if (java.lang.Double.isNaN(this.swLng)) { this.swLng = pLng } else { if (lngContains(pLng)) { return this } if (zza(this.swLng, pLng) < zzb(this.neLng, pLng)) { this.swLng = pLng return this } } this.neLng = pLng return this } // 某个经度值是否在该范围内(包含边界) private fun lngContains(lng: Double): Boolean { return if (this.swLng <= this.neLng) { this.swLng <= lng && lng <= this.neLng } else { this.swLng <= lng || lng <= this.neLng } } fun build(): LatLngBounds { // Preconditions.checkState(!java.lang.Double.isNaN(this.swLng), "no included points") return LatLngBounds(LatLng(this.swLat, this.swLng), LatLng(this.neLat, this.neLng)) } } companion object { fun builder(): Builder { return Builder() } // 前者 - 后者 private fun zza(var0: Double, var2: Double): Double { return (var0 - var2 + 360.0) % 360.0 } // 后者 - 前者 private fun zzb(var0: Double, var2: Double): Double { return (var2 - var0 + 360.0) % 360.0 } } }
LatLng就是一个x,y的点类
/** * 经纬度点 */ class LatLng( val latitude: Double, // 纬度 val longitude: Double // 经度 ) { override fun toString(): String { return "LatLng(latitude=$latitude, longitude=$longitude)" } }
注意⚠️:LatLngBounds中including()方法 和 Builder中include()方法在具体实现上是相同,但是LatLngBounds每次都会new一个新的对象,所以不适合大批量数据时使用,e.g:在计算一个点比较多的Polygon的范围时,建议使用Builder中的include()方法。
效果展示:这个Demo展示判断事发地点是否在管辖区域内,也就是判断圆心是否在某一个基础区域内,如果在基础区域内,显示圆的半径(单位英里),如果不在基础区域给予提示:Point not in jurisdiction(事发点不在管辖范围内)。
本文转自:https://blog.csdn.net/shao941122/article/details/51504519