最近使用高德地圖 JavaScript API 開發地圖應用,提煉了很多心得,故寫點博文,作個系列總結一下,但願能幫助到LBS開發同胞們。git
項目客戶端使用高德地圖 JavaScript API,主要業務爲以區縣爲基礎自由劃分區域,並將劃分好的區域存入數據庫,以做後續操做。github
開發之初便碰見一個問題,客戶能夠在城市區縣範圍內自由劃分本身須要的區域,可是高德地圖並未提供自定義區域的實現方法,因此只能藉助API自造輪子。算法
通過討論得出一個實現方法,初始加載城市區縣區域後,自定義折線對象,而後在區域內經過鼠標點擊畫出折線,再將該折線對象和已有區域邊界的路徑值一塊兒保存進數據庫,便可以構成劃分後的兩個新區域了。數據庫
研究出實現方法後,碰見了一個難題,如何判斷鼠標點擊的點是否在折線上:api
一開始但願經過判斷折線上每兩個相鄰點與鼠標點三點共線則證實點在折線上,參閱《代碼之美》後,發現了兩種解決算法:數組
在實際運用中,發現若是隻存在三個點時,計算三角形面積毫無疑問是一個優秀的算法。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 }();
測試一番,終於解決了缺陷,可以正常判斷點是否在折線上,並生成構建自定義區域及一個閉合區域所須要的最近折線點。
能夠發現隨着缺陷的不斷解決,代碼量卻愈來愈多。毫無疑問,保持代碼整潔,簡化實現邏輯是一名開發人員應有的意識。不過在過分簡化實現邏輯的過程當中,咱們是否會忽略許多用戶實際使用時將會遭遇的錯誤呢。
回顧該功能跌宕的開發流程,就會發現:
因此「因地制宜」是一種很是重要的思想,不一樣的數據結構有不一樣的優劣勢,一樣不能由於怕某種框架太「輕」,覆蓋面窄就避免使用,也不能由於框架太「重」就回避它。整日爭辯哪一種技術最好是沒有意義的,咱們須要作的是瞭解一種技術的最適使用場景,碰見該場景時使用它,享受技術開發者奉獻給使用者的那份便捷。