空間搜索-射線法

空間搜索-射線法

在配送場景下,每個商戶都有自己的配送區域,底層數據是按照geojson格式保存對應的配送區域

比如下面的多邊形對應的存儲格式和地圖上表示的範圍
{
	"type": "Feature",
	"properties": {},
	"geometry": {
		"type": "Polygon",
		"coordinates": [
			[
				[106.10595703125, 33.33970700424026],
				[106.32568359375, 32.41706632846282],
				[108.03955078125, 32.2313896627376],
				[108.25927734375, 33.15594830078649],
				[106.10595703125, 33.33970700424026]
			]
		]
	}
}



在這裏插入圖片描述

在配送場景下每個商戶都有多個如上圖的配送區域,搜索乾的事就是判斷用戶定位點是不是在這個配送區域裏面,這不就是幾何判斷邏輯嘛!?



射線法

在幾何學中,PIP(Point in Polygon)問題即判斷一點在多邊形的內部或外部。

射線法(Ray casting algorithm)是一種判斷點是否在多邊形內部的一種簡單方法。即從該點做一條射線,計算它跟多邊形邊界的交點個數,如果交點個數爲奇數,那麼點在多邊形內部,否則點在多邊形外部。

在這裏插入圖片描述
如何理解呢?
其實,對於平面內任意閉合曲線,曲線都把平面分割成了內、外兩部分。對於平面內任意一條直線,在穿越多邊形邊界時,有且只有兩種情況:進入多邊形或穿出多邊形。即:
如果點在多邊形內部,射線第一次穿越邊界一定是穿出多邊形。
如果點在多邊形外部,射線第一次穿越邊界一定是進入多邊形。
由於直線可以無限延伸,而閉合曲線包圍的區域是有限的,因此最後一次穿越多邊形邊界,一定是穿出多邊形,到達外部。

由上可推斷,從一點做一條射線,計算它跟多邊形邊界的交點個數,如果交點個數爲奇數,那麼點在多邊形內部,否則點在多邊形外部。

這裏直接上代碼

import java.io.Serializable;
import java.util.List;

/**
 * @author duson
 */
public class MyPoly {

    private List<Corner> corner;

    public List<Corner> getCorner() {
        return corner;
    }

    public void setCorner(List<Corner> corner) {
        this.corner = corner;
    }

    public boolean searchPoint(Point point) {

        if (corner == null || corner.size() == 0) {
            return false;
        }

        double x = point.origX();
        double y = point.origY();
        int points = corner.size();
        int hits = 0;
        double lastX = corner.get(points - 1).getPolyX();
        double lastY = corner.get(points - 1).getPolyY();
        double curX, curY;

        for (int i = 0; i < points; lastX = curX, lastY = curY, i++) {
            curX = corner.get(i).getPolyX();
            curY = corner.get(i).getPolyY();
            if (x == curX && y == curY) {
                return true;
            }
            if (curY == lastY) {
                continue;
            }
            double leftX;
            if (curX < lastX) {
                if (x >= lastX) {
                    continue;
                }
                leftX = curX;
            } else {
                if (x >= curX) {
                    continue;
                }
                leftX = lastX;
            }
            double test1, test2;
            if (curY < lastY) {
                if (y < curY || y >= lastY) {
                    continue;
                }
                if (x < leftX) {
                    hits++;
                    continue;
                }
                test1 = x - curX;
                test2 = y - curY;
            } else {
                if (y < lastY || y >= curY) {
                    continue;
                }
                if (x < leftX) {
                    hits++;
                    continue;
                }
                test1 = x - lastX;
                test2 = y - lastY;
            }
            if (test1 < (test2 / (lastY - curY) * (lastX - curX))) {
                hits++;
            }
        }
        return (hits & 1) != 0;
    }

    public static final class Point {

        private final double origX;

        private final double origY;

        private Point(double x, double y) {
            this.origX = x;
            this.origY = y;
        }

        public double origX() {
            return this.origX;
        }

        public double origY() {
            return this.origY;
        }
    }

    public static class Corner implements Serializable {

        private int polyX;

        private int polyY;

        public Corner(double polyX, double polyY) {
            this.polyX = (int) (polyX * 1000000d);
            this.polyY = (int) (polyY * 1000000d);
        }

        public double getPolyX() {
            return polyX / 1000000d;
        }

        public void setPolyX(int polyX) {
            this.polyX = polyX;
        }

        public double getPolyY() {
            return polyY / 1000000d;
        }

        public void setPolyY(int polyY) {
            this.polyY = polyY;
        }
    }
}

上面代碼有些細節

1:點存儲爲啥用int類型,不用float?double?

最開始存儲多邊形點用的是float,double類型的經緯度轉換成float類型小數點後可能會截到5位,在配送場景下,小數點後5位可能就誤差10m(隔一條馬路的距離),這個時候要不只能改成double類型,這樣精度是不會有損,但是索引會翻倍,怎麼辦呢?
想了個辦法,在業務上多邊形點的精度只要保留到小數點後6-7位就滿足了,代碼上對點的(int)(經度/緯度*100 0000),比如 (int)(106.10595703125*100 0000)=106105957,射線法判斷的時候在除以100 0000即可,這樣存儲空間和之前的存float類型的是一樣的,還能保證誤差在可接受範圍內。



2: 射線法爲啥不用網上的?

最開始也試了很多網上實現的方式,發現針對某些case都會有誤判的情況,最後找的jdk裏面的 「 java.awt.Polygon#contains方法」的代碼,這個方法未發現漏判和誤判的情況,目前線上都用的是這個方法,穩定運行3年多。



總結:

射線法只是空間搜索很小的點,真正線上使用的時候也是和geohash或者rtree/r*tree 空間索引配合使用

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