高德地圖API開發二三事(一)如何判斷點是否在折線上及引伸思考

最近使用高德地圖 JavaScript API 開發地圖應用,提煉了很多心得,故寫點博文,作個系列總結一下,但願能幫助到LBS開發同胞們。git

項目客戶端使用高德地圖 JavaScript API,主要業務爲以區縣爲基礎自由劃分區域,並將劃分好的區域存入數據庫,以做後續操做github

開發之初便碰見一個問題,客戶能夠在城市區縣範圍內自由劃分本身須要的區域,可是高德地圖並未提供自定義區域的實現方法,因此只能藉助API自造輪子。算法

通過討論得出一個實現方法,初始加載城市區縣區域後,自定義折線對象,而後在區域內經過鼠標點擊畫出折線,再將該折線對象和已有區域邊界的路徑值一塊兒保存進數據庫,便可以構成劃分後的兩個新區域了。數據庫

  • 研究出實現方法後,碰見了一個難題,如何判斷鼠標點擊的點是否在折線上:api

    • 畫起點和終點時必須在原有區域線上,不然沒法造成新的封閉空間。故須要判斷鼠標點擊的點是否在原有折線上,在就讓其成爲起點或終點,不在則讓其從新點擊。
    • 可是用戶點擊時沒法保證徹底點擊在原有折線上,故須要容許必定的偏差,在偏差內則判斷爲點在折線上,偏差外讓其從新點擊。
    • 判斷爲在偏差內後,鼠標點終究不在折線上,此時須要在原折線上生成一個新的點(離該鼠標點最近的點)

一開始但願經過判斷折線上每兩個相鄰點與鼠標點三點共線則證實點在折線上,參閱《代碼之美》後,發現了兩種解決算法:數組

  • 一種是判斷斜率相等,可是因爲一下問題被《代碼之美》否決,並提出了更加優化的方法。
    • 判斷斜率相等存在多種特殊狀況,如兩點經度相等或者緯度相等時,代碼實現過於繁瑣。
    • 斜率使用除法計算爲浮點數,存在必定偏差。
  • 更優化的方法爲三點能夠組成一個三角形,當三角形面積接近於0時,則判斷點在線上。
  • 具體細節能夠參看《代碼之美》第33章。

在實際運用中,發現若是隻存在三個點時,計算三角形面積毫無疑問是一個優秀的算法。markdown

可是如前文提到的,鼠標點沒法精確點擊在折線上,故須要容許必定偏差,也就是說三角形面積沒法等於0,只能遍歷折線每兩個相鄰點,計算鼠標點與兩點組成的三角形面積,取出最小的面積,當其小於一個偏差值時,點在折線上。數據結構

這樣就可能會產生缺陷。一個折線對象存在着數以千計的相鄰點,當鼠標點與折線上某兩個相鄰點組成的三角形面積最小時,卻沒法保證該點必定離這兩個相鄰點最近。框架

理想狀況下是這樣的,理想狀況示意圖三角形面積最小,而且鼠標點離該兩點組成的線段最近。而特殊狀況下會是這樣的,特殊狀況示意圖三角形面積一樣最小,但鼠標點其實離線段較遠。測試

無奈只能另尋解決方法,而後在百度LBS JavaScript開源庫中發現幾何運算類提供了判斷線是否在折線上的方法isPointOnPolyline(),大喜,趕忙研究一番,應用在項目中。

 1  /**
 2      * 判斷點是否在矩形內
 3      * @param {Point} point 點對象
 4      * @param {Bounds} bounds 矩形邊界對象
 5      * @returns {Boolean} 點在矩形內返回true,不然返回false
 6      */
 7     function isPointInRect(point, bounds) {
 8         var sw = bounds.getSouthWest(); //西南腳點
 9         var ne = bounds.getNorthEast(); //東北腳點
10         return (point.lng >= sw.lng && point.lng <= ne.lng && point.lat >= sw.lat && point.lat <= ne.lat);
11     }
12 
13     /**
14      * 判斷點是否在折線上
15      * @param {Point} point 點對象
16      * @param {Polyline} polyline 折線對象
17      * @returns {Boolean} 點在折線上返回true,不然返回false
18      */
19     function isPointOnPolyline(point, polyline){
20         //首先判斷點是否在線的外包矩形內,若是在,則進一步判斷,不然返回false
21         var lineBounds = polyline.getBounds();
22         if(!this.isPointInRect(point, lineBounds)){
23             return false;
24         }
25         //判斷點是否在線段上,設點爲Q,線段爲P1P2 ,
26         //判斷點Q在該線段上的依據是:( Q - P1 ) × ( P2 - P1 ) = 0,且 Q 在以 P1,P2爲對角頂點的矩形內
27         var pts = polyline.getPath();
28         for(var i = 0; i < pts.length - 1; i++){
29             var curPt = pts[i];
30             var nextPt = pts[i + 1];
31             //首先判斷point是否在curPt和nextPt之間,即:此判斷該點是否在該線段的外包矩形內,先判斷離point最近的兩個相鄰點,再進行斜率計算,有效避免干擾
32             if (point.lng >= Math.min(curPt.lng, nextPt.lng) && point.lng <= Math.max(curPt.lng, nextPt.lng) &&
33                 point.lat >= Math.min(curPt.lat, nextPt.lat) && point.lat <= Math.max(curPt.lat, nextPt.lat)){
34                 //判斷點是否在直線上公式,此處使用減法計算兩個斜率之差,有效地簡化了特殊狀況的判斷
35                 var precision = (curPt.lng - point.lng) * (nextPt.lat - point.lat) - 
36                     (nextPt.lng - point.lng) * (curPt.lat - point.lat);                
37                 if(precision < 2e-10 && precision > -2e-10){//實質判斷是否接近0
38                     return true;
39                 }                
40             }
41         }
42 
43         return false;
44     }

測一測項目,哈哈,可行,長舒一口氣。正準備好好放鬆下,OMG!又碰見缺陷了,以下圖北京西城區存在的一個狀況,
北京西城區缺陷演示
此時折線上相鄰兩點的經度幾乎相等,或者北京豐臺區存在的狀況,
北京豐臺區缺陷演示
此時折線上相鄰兩點的緯度幾乎相等。

因爲方法優先判斷鼠標點是否在折線某相鄰兩點的外包矩形內,可是上述兩種狀況下,相鄰兩點的外包矩形幾乎爲0,則鼠標點只有在精確點擊到折線的狀況下才會判斷爲true。這與實際開發中要求容許必定偏差是相悖的,無奈只能另尋解決方法。

皇天不負有心人,在兩次推翻實現算法後,終於又找到一種解決方法。遍歷折線對象取出全部相鄰點,計算鼠標點到每兩個相鄰點組成的線段的最短距離,而後排序最短距離,取出其中最小的距離,若是小於偏差範圍,則判斷點在折線上。若是須要閉合區間,則在折線上生成一個離鼠標點最近的折線點(通常取垂足經緯度)。實現代碼以下(如下實現代碼已分享至https://github.com/nitta-honoka/LBSUtilsExtension):

  1 /**
  2  *計算折線是否在線上部分:主要實現算法爲計算鼠標點到折線上每相鄰兩點組成的線段的最短距離,若是最小的最短距離小於偏差值,
  3  *則判斷點在折線上。
  4  *而後經過該相鄰兩點取得折線上離鼠標點最近的點。
  5  */
  6 isPointOnPloyline = function() {
  7   /**
  8    * 計算兩點之間的距離
  9    * @param x1 第一個點的經度
 10    * @param y1 第一個點的緯度
 11    * @param x2 第二個點的經度
 12    * @param y1 第二個點的緯度
 13    * @returns lineLength 兩點之間的距離
 14    */
 15   function lineDis(x1, y1, x2, y2) {
 16     var lineLength = 0;
 17     lineLength = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
 18     return lineLength;
 19   }
 20   /**
 21    * 計算鼠標點到折線上相鄰兩點組成的線段的最短距離
 22    * @param point 鼠標點
 23    * @param curPt 折線點
 24    * @param nextPt 與curPt相鄰的折線點
 25    * @returns dis 最短距離
 26    */
 27   function countDisPoToLine(point, curPt, nextPt) {
 28     var dis = 0; //鼠標點到線段的最短距離
 29     var xCur = curPt.lng; //折線點的經緯度,將該點記做P1
 30     var yCur = curPt.lat;
 31     var xNext = nextPt.lng; //與上一個取點相鄰的折線點的經緯度,將該點記做P2
 32     var yNext = nextPt.lat;
 33     var xPoint = point.lng; //鼠標點的經緯度,將該點記做P
 34     var yPoint = point.lat;
 35     var lengthCurToPo = lineDis(xCur, yCur, xPoint, yPoint); //P1到P的長度,記做b線段
 36     var lengthNextToPo = lineDis(xNext, yNext, xPoint, yPoint); //P2到P的長度,記做c線段
 37     var lengthCurToNext = lineDis(xCur, yCur, xNext, yNext); //P1到P2的長度,記做a線段
 38 
 39     if (lengthNextToPo + lengthCurToPo == lengthCurToNext) {
 40       //當b+c=a時,P在P1和P2組成的線段上
 41       dis = 0;
 42       return dis;
 43     } else if (lengthNextToPo * lengthNextToPo >= lengthCurToNext * lengthCurToNext + lengthCurToPo * lengthCurToPo) {
 44       //當c*c>=a*a+b*b時組成直角三角形或鈍角三角形,投影在P1延長線上
 45       dis = lengthCurToPo;
 46       return dis;
 47     } else if (lengthCurToPo * lengthCurToPo >= lengthCurToNext * lengthCurToNext + lengthNextToPo * lengthNextToPo) {
 48       //當b*b>c*c+a*a時組成直角三角形或鈍角三角形,投影在p2延長線上
 49       dis = lengthNextToPo;
 50       return dis;
 51     } else {
 52       //其餘狀況組成銳角三角形,則求三角形的高
 53       var p = (lengthCurToPo + lengthNextToPo + lengthCurToNext) / 2; // 半周長
 54       var s = Math.sqrt(p * (p - lengthCurToNext) * (p - lengthCurToPo) * (p - lengthNextToPo)); // 海倫公式求面積
 55       dis = 2 * s / lengthCurToNext; // 返回點到線的距離(利用三角形面積公式求高)
 56       return dis;
 57     }
 58   }
 59   /**
 60    * 判斷點是否在矩形內
 61    * @param point 點對象
 62    * @param bounds 矩形邊界對象
 63    * @returns 點在矩形內返回true,不然返回false
 64    */
 65   function isPointInRect(point, bounds) {
 66     var sw = bounds.getSouthWest(); //西南腳點
 67     var ne = bounds.getNorthEast(); //東北腳點
 68 
 69     return (point.lng >= sw.lng && point.lng <= ne.lng && point.lat >= sw.lat && point.lat <= ne.lat);
 70 
 71   }
 72 
 73   /**
 74    * 判斷點是否在折線上,若是須要在折線上生成最近點,則使用下一個方法
 75    * @param point 鼠標點
 76    * @param polygon 區域多邊形對象
 77    * @returns 若是判斷點不在折線上則返回false,不然返回true
 78    */
 79   function isPointOnPloylineTest(point, polygon) {
 80     // 首先判斷點是否在線的外包矩形內,若是在,則進一步判斷,不然返回false
 81     var lineBounds = polygon.getBounds();
 82     if (!isPointInRect(point, lineBounds)) {
 83       return false;
 84     }
 85     var disArray = new Array(); //存儲最短距離
 86     var pts = polygon.getPath();
 87     var curPt = null; //折線的兩個相鄰點
 88     var nextPt = null;
 89     for (var i = 0; i < pts.length - 1; i++) {
 90       curPt = pts[i];
 91       nextPt = pts[i + 1];
 92       //計算鼠標點到該兩個相鄰點組成的線段的最短距離
 93       var dis = countDisPoToLine(point, curPt, nextPt);
 94       //先將存儲最短距離的數組排序
 95       disArray.push(dis);
 96       disArray.sort();
 97 
 98     }
 99     var disMin = disArray[0]; //取得數組中最小的最短距離
100     if (disMin < 2e-4 && disMin > -2e-4) { //當最短距離小於偏差值時,判斷鼠標點在折線上(偏差值可根據須要更改)
101       return true;
102     }
103     return false;
104   }
105   /**
106    * 判斷點是否在折線上,若是判斷爲真則在折線上生成離該點最近的點,不然返回鼠標點座標
107    * @param point 鼠標點
108    * @param polygon 區域多邊形對象
109    * @returns 若是判斷點不在折線上則返回該點(point),若是判斷點在折線上則返回計算出的折線最近點(
110    由於鼠標點選很難精確點在折線上,要容許必定偏差,故需生成一個折線上的最近點),
111    返回該最近點(pointPoly)。
112    */
113   function isPointOnPloylineTest_02(point, polygon) {
114     // 首先判斷點是否在線的外包矩形內,若是在,則進一步判斷,不然返回false
115     var lineBounds = polygon.getBounds();
116     if (!isPointInRect(point, lineBounds)) {
117       return point;
118     }
119     var disArray = new Array(); //存儲最短距離
120     var pointArray = new Array(); //存儲折線相鄰點
121     var pts = polygon.getPath();
122     var curPt = null; //折線的兩個相鄰點
123     var nextPt = null;
124     for (var i = 0; i < pts.length - 1; i++) {
125       curPt = pts[i];
126       nextPt = pts[i + 1];
127       //計算鼠標點到該兩個相鄰點組成的線段的最短距離
128       var dis = countDisPoToLine(point, curPt, nextPt);
129       //先將存儲最短距離的數組排序,若是該兩個相鄰點與鼠標點計算出的最短距離與數組中最小距離相等,則存儲該兩點
130       disArray.push(dis);
131       disArray.sort();
132       if (dis == disArray[0]) {
133         pointArray.push(curPt);
134         pointArray.push(nextPt);
135 
136       }
137     }
138 
139     curPt = pointArray[pointArray.length - 2]; //取得數組最後兩項,即爲當最短距離最小時鼠標點兩側的折線點
140     nextPt = pointArray[pointArray.length - 1];
141     var disMin = disArray[0]; //取得數組中最小的最短距離
142 
143 
144     if (disMin < 2e-4 && disMin > -2e-4) { //當最短距離小於偏差值時,判斷鼠標點在折線上(偏差值可根據須要更改)
145       var pointPoly = getPointOnPolyline(point, curPt, nextPt); //經過鼠標點和兩側相鄰點,在折線上生成一個距離鼠標點最近的點
146       return pointPoly;
147     }
148     return point;
149   }
150 
151   /**
152    * 若是點離折線上某兩點組成的線段最近,則在折線上生成與鼠標點最近的折線點
153    * @param point 鼠標點
154    * @param curPt,nextPt 折線上相鄰兩點
155    * @returns pointPoly 生成點
156    */
157   function getPointOnPolyline(point, curPt, nextPt) {
158     var pointLng; // 取得點的經度
159     var pointLat; // 取得點的緯度
160     var precisionLng = curPt.lng - nextPt.lng;
161     var precisionLat = curPt.lat - nextPt.lat;
162 
163     if (precisionLng < 2e-6 && precisionLng > -2e-6) {
164       // 當折線上兩點經度幾乎相同時(存在必定偏差)
165       pointLng = curPt.lng;
166       pointLat = point.lat;
167       //建立生成點對象
168       var pointPoly = new AMap.LngLat(curPt.lng, pointLat);
169     } else if (precisionLat < 2e-6 && precisionLat > -2e-6) {
170       //當折線上兩點緯度相同時(存在必定偏差)
171       pointLat = curPt.lat;
172       pointLng = point.lng;
173       var pointPoly = new AMap.LngLat(pointLng, curPt.lat);
174     } else {
175       //其餘狀況,求得點到折線的垂足座標
176       var k = (nextPt.lat - curPt.lat) / (nextPt.lng - curPt.lng); //折線上兩點組成線段的斜率
177       //求得該點到線段的垂足座標
178       //設線段的兩端點爲pt1和pt2,斜率爲:k = ( pt2.y - pt1. y ) / (pt2.x - pt1.x );
179       //該直線方程爲:y = k* ( x - pt1.x) + pt1.y。其垂線的斜率爲 - 1 / k,
180       //垂線方程爲:y = (-1/k) * (x - point.x) + point.y
181       var pointLng_02 = (k * k * curPt.lng + k * (point.lat - curPt.lat) + point.lng) / (k * k + 1);
182       var pointLat_02 = k * (pointLng_02 - curPt.lng) + curPt.lat;
183       var pointPoly = new AMap.LngLat(pointLng_02, pointLat_02);
184     }
185     return pointPoly;
186   }
187   return {
188     //只需判斷調用第一個別名,須要生成點調用第二個別名
189     isPointOnPloylineTest: isPointOnPloylineTest;
190     isPointOnPloylineTest_02: isPointOnPloylineTest_02;
191   }
192 }();

測試一番,終於解決了缺陷,可以正常判斷點是否在折線上,並生成構建自定義區域及一個閉合區域所須要的最近折線點。

能夠發現隨着缺陷的不斷解決,代碼量卻愈來愈多。毫無疑問,保持代碼整潔,簡化實現邏輯是一名開發人員應有的意識。不過在過分簡化實現邏輯的過程當中,咱們是否會忽略許多用戶實際使用時將會遭遇的錯誤呢。

回顧該功能跌宕的開發流程,就會發現:

  • 若是折線不是一個閉合空間,而僅僅是較少點組成的幾段線段時,三角形面積的算法碰見的缺陷沒有出現的機會,將是最適合的算法。
  • 若是折線點較多,可是其中不存在經度或緯度幾乎相等的相鄰點時,百度提供的算法又將是最適合的算法。
  • 若是折線點較多,且狀況複雜時,採用最後「較重」的算法,才能避免缺陷,成爲一枚正常運轉的齒輪。

因此「因地制宜」是一種很是重要的思想,不一樣的數據結構有不一樣的優劣勢,一樣不能由於怕某種框架太「輕」,覆蓋面窄就避免使用,也不能由於框架太「重」就回避它。整日爭辯哪一種技術最好是沒有意義的,咱們須要作的是瞭解一種技術的最適使用場景,碰見該場景時使用它,享受技術開發者奉獻給使用者的那份便捷。

相關文章
相關標籤/搜索