地理圍欄(Geo-fencing)是LBS的一種應用,就是用一個虛擬的柵欄圍出一個虛擬地理邊界,當手機進入、離開某個特定地理區域,或在該區域內活動時,手機可以接收自動通知和警告。如下圖所示,假設地圖上有三個商場,當使用者進入某個商場的時候,手機自動收到相應商場傳送的優惠券push訊息。地理圍欄應用非常廣泛,當今移動網際網路主要app如美團、大眾點評、手淘等都可看到其應用身影。
圖1 地理圍欄示意圖
地理圍欄的核心問題就是判斷使用者是否落在某多邊形圍欄內部。本文將介紹實際應用中常用的解決方法。
1 如何判斷點在多邊形內部
地理圍欄一般是多邊形,如何判斷點在多邊形內部呢?可以通過射線法來判斷點是否在多邊形內部。如下圖所示,從該點出發沿著X軸畫一條射線,依次判斷該射線與每條邊的交點,並統計交點個數,如果交點數為奇數,則在多邊形內部(如圖3個交點),如果焦點數是偶數,則在外部,射線法對凸和非凸多邊形都適用,複雜度為O(N),其它N是邊數。原始碼可參考(http://alienryderflex.com/polygon/)
圖2 射線法判斷點在多邊形內外
當地理圍欄多邊形數目較少時,我們可以依次遍歷每一個多邊形(暴力遍歷法),然後用射線法進行判斷,這樣效率也很高。而當多邊形數目較多時,比如有10萬個多邊形,這個時候需要執行10萬次射線法,響應時間達到3.9秒,這在網際網路應用幾乎不可忍受。下表是本人的簡單測試,多邊形邊數均為7。
表1 射線法效能測試
2 R樹索引加速判斷
暴力遍歷法效率低下的原因是與每一個多邊形都進行了射線法判斷,如果能減少射線法的呼叫次數效能就能提升。因此我們的優化思路很直接,首先通過粗篩的方法快速找到符合條件的少量多邊形,然後對粗篩後的多邊形使用射線法判斷,這樣射線法的執行次數大大降低,效率也能大大提高。怎麼粗篩呢?對於一維資料我們常常使用索引的方法,比如通過B樹索引找到某一個範圍區間段,然後對此範圍區間段進行遍歷查詢,對於二維空間資料常常使用空間索引的方法,比如通過R樹找到範圍區間內的多邊形,然後對此範圍內的多邊形進行精確判斷,下面介紹最常使用的空間索引R樹的解決思路。
1)外包矩形表示多邊形
由於多邊形形狀各異,我們需要以一種統一的方式來對多邊形進行近似,最簡單的方式就是用最小外包矩形來表示多邊形。
圖3 最小外包矩形(MBR)表達多邊形
2)對最小外包矩形建立R樹索引
圖4 對最小外包矩形進行R樹索引
3)查詢
a)首先通過R樹迅速判斷使用者所在位置(粗紅點)是否被外包矩形覆蓋(圖5,紅色點代表使用者所在位置;R樹平均查詢複雜度為O(Log(N)),N為多邊形個數);
b)如果不被任何外包矩形覆蓋則返回不在地理圍欄多邊形內;
c)如果被外包矩形覆蓋則還需要進一步判斷是否在此外包矩形的多邊形內部,採用上文提到的射線法判斷(圖2)。
圖5 R樹查詢示例
3 多邊形邊數較多怎麼辦
大多數應用的地理圍欄多邊形都比較簡單,但有時也會遇到一些特別複雜的多邊形,比如單個多邊形的邊數就超過十幾萬條,這時候對此複雜多邊形執行一次射線法也非常耗時(因為射線法時間複雜度為O(N),N為多邊形邊數)。
如何提高對複雜多邊形執行射線法的計算效率呢?同樣使用R樹索引!筆者在實際應用中對邊數較多(如超過1萬)的多邊形的邊再單獨進行R樹索引,具體如圖6所示,首先對多邊形的每條邊構建最小外包矩形,然後在這些最小外包矩形基礎上構建R樹索引(R樹索引上的外包矩形未畫出),這樣射線法求交點的時候首先通過R樹判斷射線是否與外包矩形相交,最後對R樹粗篩後的邊進行精確求交判斷,時間複雜度從O(N)降到O(Log(N)),大大提高了計算效率。
圖6 對多邊形的邊進行R樹索引
4 實踐
某線上應用服務有30萬個地理圍欄多邊形,通過在記憶體中構建R樹索引,使得線上實時地理圍欄查詢平均響應時間在1ms以內,而暴力查詢響應時間是9秒左右。
5 R樹相關原始碼
https://pypi.python.org/pypi/Rtree/ (Python)
http://jsi.sourceforge.net/ (Java)
https://github.com/leaflet-extras/RTree (Javascript)
http://sourceforge.net/p/cspatialindexrt/code/HEAD/tree/ (C#)