[幾何] 判斷點是否在任意多邊形內

最近項目用到:在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

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見~

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章