[几何] 判断点是否在任意多边形内

最近项目用到:在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:GitHub - shaoshuai904/GoogleMap_Demo: GoogleMap一些常用API总结库

GitHub项目代码中,包含Google map原生代码,和Kotlin版本 ,Java版本抽取代码,(Kotlin和Java版本标识了@Deprecated,自己使用都时候去掉就好了)各位老铁,根据自己的需要 复制几个类就好了,没必要为了一个函数,添加一个jar包。

下面是代码部分:

/**
 * 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包的麻烦


/**
 * 经纬度范围类
 * <p>
 * 复写com.google.android.gms.maps.model.LatLngBounds中核心方法
 *
 * @author maple
 * @time 2019-05-28
 */
@Deprecated //("条件允许,请使用com.google.android.gms.maps.model.LatLngBounds")
public class LatLngBoundsJava {
    public final JLatLng southwest;// 左下角 点
    public final JLatLng northeast;// 右上角 点

    private LatLngBoundsJava(JLatLng southwest, JLatLng northeast) {
//        Preconditions.checkNotNull(southwest, "null southwest");
//        Preconditions.checkNotNull(northeast, "null northeast");
//        Preconditions.checkArgument(northeast.latitude >= southwest.latitude, "southern latitude exceeds northern latitude (%s > %s)", new Object[]{southwest.latitude, northeast.latitude});
        this.southwest = southwest;
        this.northeast = northeast;
    }

    // 获取中心点
    public JLatLng getCenter() {
        // 计算中心点纬度
        double centerLat = (this.southwest.latitude + this.northeast.latitude) / 2.0;
        // 计算中心点经度
        double neLng = this.northeast.longitude;// 右上角 经度
        double swLng = this.southwest.longitude; // 左下角 经度
        double centerLng;
        if (swLng <= neLng) {
            centerLng = (neLng + swLng) / 2.0;
        } else {
            centerLng = (neLng + 360.0 + swLng) / 2.0;
        }
        return new JLatLng(centerLat, centerLng);
    }

    // 小数据量可以使用该方法,大数据量建议使用Builder中的include()
    public LatLngBoundsJava including(JLatLng point) {
        double swLat = Math.min(this.southwest.latitude, point.latitude);
        double neLat = Math.max(this.northeast.latitude, point.latitude);
        double neLng = this.northeast.longitude;
        double swLng = this.southwest.longitude;
        double pLng = point.longitude;
        if (!this.lngContains(pLng)) {
            if (zza(swLng, pLng) < zzb(neLng, pLng)) {
                swLng = pLng;
            } else {
                neLng = pLng;
            }
        }
        return new LatLngBoundsJava(new JLatLng(swLat, swLng), new JLatLng(neLat, neLng));
    }

    // 某个点是否在该范围内(包含边界)
    public boolean contains(JLatLng point) {
        return latContains(point.latitude) && this.lngContains(point.longitude);
    }

    // 某个纬度值是否在该范围内(包含边界)
    public boolean latContains(Double lat) {
        return this.southwest.latitude <= lat && lat <= this.northeast.latitude;
    }

    // 某个经度值是否在该范围内(包含边界)
    public boolean lngContains(Double lng) {
        if (this.southwest.longitude <= this.northeast.longitude) {
            return this.southwest.longitude <= lng && lng <= this.northeast.longitude;
        } else {
            return this.southwest.longitude <= lng || lng <= this.northeast.longitude;
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    // 前者 - 后者
    private static double zza(double var0, double var2) {
        return (var0 - var2 + 360.0D) % 360.0D;
    }

    // 后者 - 前者
    private static double zzb(double var0, double var2) {
        return (var2 - var0 + 360.0D) % 360.0D;
    }

    /**
     * LatLngBounds生成器
     */
    @Deprecated //("条件允许,请使用com.google.android.gms.maps.model.LatLng")
    public static final class Builder {
        private double swLat = 1.0D / 0.0; // 左下角 纬度
        private double swLng = 0.0D / 0.0; // 左下角 经度
        private double neLat = -1.0D / 0.0; // 右上角 纬度
        private double neLng = 0.0D / 0.0; // 右上角 经度

        public Builder() {
        }

        public final Builder include(JLatLng point) {
            this.swLat = Math.min(this.swLat, point.latitude);
            this.neLat = Math.max(this.neLat, point.latitude);
            double pLng = point.longitude;
            if (Double.isNaN(this.swLng)) {
                this.swLng = pLng;
            } else {
                // 某个经度值是否在该范围内(包含边界)
                if (this.swLng <= this.neLng ?
                        this.swLng <= pLng && pLng <= this.neLng :
                        this.swLng <= pLng || pLng <= this.neLng) {
                    return this;
                }

                if (zza(this.swLng, pLng) < zzb(this.neLng, pLng)) {
                    this.swLng = pLng;
                    return this;
                }
            }

            this.neLng = pLng;
            return this;
        }

        public final LatLngBoundsJava build() {
            // Preconditions.checkState(!Double.isNaN(this.swLng), "no included points");
            return new LatLngBoundsJava(new JLatLng(this.swLat, this.swLng), new JLatLng(this.neLat, this.neLng));
        }
    }

    @Deprecated //("条件允许,请使用com.google.android.gms.maps.model.LatLng")
    public static final class JLatLng {
        public final double latitude;
        public final double longitude;

        public JLatLng(double lat, double lng) {
            if (-180.0D <= lng && lng < 180.0D) {
                // 经度合格,直接赋值
                this.longitude = lng;
            } else {
                // 修正经度。经度必须在[-180,180]之间
                this.longitude = ((lng - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
            }
            // 纬度必须在[-90,90]范围内
            this.latitude = Math.max(-90.0D, Math.min(90.0D, lat));
        }

    }
}

LatLng就是一个x,y的点类,以下是同款类JLatLng.

    @Deprecated //("条件允许,请使用com.google.android.gms.maps.model.LatLng")
    public static final class JLatLng {
        public final double latitude;
        public final double longitude;

        public JLatLng(double lat, double lng) {
            if (-180.0D <= lng && lng < 180.0D) {
                // 经度合格,直接赋值
                this.longitude = lng;
            } else {
                // 修正经度。经度必须在[-180,180]之间
                this.longitude = ((lng - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D;
            }
            // 纬度必须在[-90,90]范围内
            this.latitude = Math.max(-90.0D, Math.min(90.0D, lat));
        }

    }

注意⚠️:LatLngBounds中including()方法  和 Builder中include()方法在具体实现上是相同,但是LatLngBounds每次都会new一个新的对象,所以不适合大批量数据时使用,e.g:在计算一个点比较多的Polygon的范围时,建议使用Builder中的include()方法。

效果展示:这个Demo展示判断事发地点是否在管辖区域内,也就是判断圆心是否在某一个基础区域内,如果在基础区域内,显示圆的半径(单位英里),如果不在基础区域给予提示:Point not in jurisdiction(事发点不在管辖范围内)。

动画失效了,懒得补了,Github见~

GitHub:GitHub - shaoshuai904/GoogleMap_Demo: GoogleMap一些常用API总结库

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 36
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值