最近作項目的時候遇到的一個需求:要實現一個鏈家網地圖找房中的畫圈找房功能。鏈家網是採用百度地圖實現房源展現,先來看看鏈家網的畫圈找房功能,有木有很炫酷~~,能夠到鏈家上體驗一下
鏈家網畫圈找房效果 javascript
主要是想分享下在完成這個畫圈找房功能的過程當中,面對沒有現成api調用或者方案的問題,本身的思路過程以及遇到的一些問題是怎麼解決的
css
此demo未採用框架,用原生js實現,項目裏是用react技術棧實現,打開你最喜歡的IDE,新建以下3個文件draw.js, draw.css, draw.html, 我這裏是用webstorm編輯代碼,demo結構以下圖 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>鏈家網畫圈找房demo</title>
<link href="draw.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="wrapper">
<div class="map-container" id="container">
</div>
<div class="panel">
<div class="top">
<button class="btn" id="draw">畫圈找房</button>
<button class="btn" id="exit">退出畫圈</button>
</div>
<div class="bottom">
<ul id="data">
</ul>
</div>
</div>
</div>
<script type="text/javascript" src="draw.js"></script>
</body>
</html>
複製代碼
html,body{
margin:0;
padding:0;
height:100%;
min-width:800px;
}
ul,li{
margin:0;
padding:0;
}
.wrapper{
height:100%;
padding-right:300px;
}
.map-container{
height:100%;
width:100%;
float:left;
}
.panel{
float:left;
margin-left:-300px;
width:300px;
height:100%;
position: relative;
right:-300px;
box-shadow: -2px 2px 2px #d9d9d9;
}
.top{
height:150px;
padding:10px;
border-bottom: 1px solid #bfbfbf;
}
.bottom{
position: absolute;
top:171px;
bottom:0;
width:100%;
}
.btn{
outline:none;
border:none;
display: block;
margin: 20px auto;
font-size: 17px;
color:#fff;
border-radius: 4px;
padding:8px;
background-color: #969696;
cursor:pointer;
transition: all .5s;
}
.btn:hover{
background-color: #b8b8b8;
}
#data li {
width:100%;
height:50px;
border-bottom: 1px dashed #bfbfbf;
padding:10px 20px;
list-style-type: none;
line-height: 50px;
color: #737373;
}
複製代碼
上述實現了一個左側自適應右側固定寬度的佈局,左側容器用於展現百度地圖,右側是操做面板,頁面以下圖所示,點擊畫圈找房進入畫圈狀態,點擊退出畫圈按鈕進入正常操做地圖狀態, 按鈕下面是顯示數據列表部分java
本demo須要地圖的支持,這裏採用百度地圖,騰訊地圖和高德地圖也應該能夠實現本demo的效果,首先登陸百度地圖開放平臺進行帳號註冊,若是已有百度帳號能夠不用註冊
在使用百度地圖服務前須要申請密鑰(ak), 點擊 開發文檔 -> JavaScript API進入Javascript指南部分,按照指南註冊好本身的密鑰(ak)react
而後在添加百度地圖腳本到draw.html中,而後把ak後面的中文換成剛剛申請的密鑰便可git
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=您的密鑰">
複製代碼
最後在draw.js裏寫下以下代碼,百度地圖初始化就完了成了,運行draw.html能夠看到地圖已經展現出來了(見下圖)!第一句代碼裏的container是地圖容器的id,咱們選擇北京做爲展現座標github
window.onload = function(){
var map = new BMap.Map("container"); // 建立地圖實例
var point = new BMap.Point(116.404, 39.915); // 建立點座標
map.centerAndZoom(point, 15); // 初始化地圖,設置中心點座標和地圖級別
map.enableScrollWheelZoom(true); // 開啓鼠標滾輪縮放
}
複製代碼
咱們須要在地圖上放置幾個點做爲畫圈的初始數據,查閱百度地圖API,寫下在draw.js裏寫以下函數進行初始化,並在window.onload中調用該方法web
//初始化地圖座標點
function initMapMarkers(map){
//地圖上須要標註點的座標信息(經度,緯度,文本描述)
var dataList = [
[116.351951,39.929543,'北京國賓酒店'],
[116.404556,39.92069,'故宮博物院'],
[116.479008,39.925781,'呼家樓'],
[116.368624,39.870869,'首都醫科大學'],
[116.4471,39.849601,'宋家莊']
];
//建立marker和label(文字標籤)並顯示在地圖上
dataList.forEach(function(item){
var point = new BMap.Point(item[0],item[1])
var marker = new BMap.Marker(point);
var label = new BMap.Label(item[2],{offset:new BMap.Size(20,-10)});
marker.setLabel(label);
markerList.push(marker);
map.addOverlay(marker)
})
}
複製代碼
刷新頁面發現地圖上已經有了點數據和標註
/*** 界面元素 ***/
//畫圈按鈕
var drawBtn = document.getElementById('draw')
//退出畫圈按鈕
var exitBtn = document.getElementById('exit')
//畫圈完成的數據展現列表
var ul = document.getElementById('data')
/*** 畫圈有關的數據結構 ***/
//是否處於畫圈狀態下
var isInDrawing = false;
//是否處於鼠標左鍵按下狀態下
var isMouseDown = false;
//存儲畫出折線點的數組
var polyPointArray = [];
//上次操做畫出的折線
var lastPolyLine = null;
//畫圈完成後生成的多邊形
var polygonAfterDraw = null;
//存儲地圖上marker的數組
var markerList = [];
複製代碼
整個demo的基本邏輯以下:
點擊畫圈找房按鈕進入畫圈狀態,在畫圈狀態下,在地圖上按下鼠標左鍵開始畫圈操做,移動鼠標進行畫圈操做,而後擡起鼠標左鍵完成畫圈,此時地圖上會顯示出圈,而後右列表會顯示出圈內座標的文本。最後點擊退出畫圈按鈕退出畫圈狀態
所以須要爲地圖以及按鈕綁定事件,寫下以下函數進行事件綁定:api
//開始畫圈綁定事件
drawBtn.addEventListener('click',function(e){
//禁止地圖移動點擊等操做
map.disableDragging();
map.disableScrollWheelZoom();
map.disableDoubleClickZoom();
map.disableKeyboard();
map.setDefaultCursor('crosshair');
//設置標誌位進入畫圈狀態
isInDrawing = true;
});
//退出畫圈按鈕綁定事件
exitBtn.addEventListener('click',function(e){
//恢復地圖移動點擊等操做
map.enableDragging();
map.enableScrollWheelZoom();
map.enableDoubleClickZoom();
map.enableKeyboard();
map.setDefaultCursor('default');
//設置標誌位退出畫圈狀態
isInDrawing = false;
})
複製代碼
這是本demo的第一個難點,如何實現鏈家的這種相似畫筆的畫操做?我翻看了百度地圖關於畫圖的全部api後,只發現百度地圖提供了繪製圓,直線,多邊形,矩形的api,官網demo以下所示:數組
回去繼續查閱百度地圖api,發現有在地圖上畫出折線的api,其參數是一個由Point組成的數組,只要給出這個數組就能畫出折線來,點擊這裏前往百度地圖api
map.addEventListener('mousemove',function(e){
console.log(e.point)
})
複製代碼
既然點能獲取到了,那麼就用一個數組把這些點保存下來用於後續畫線操做。而後整個畫線邏輯就很明顯了:每次mousemove觸發都往數組中push當前鼠標所在點,而後調用api進行畫線,同時用一個lastPolyLine變量記錄下上次畫的線,由於每次mousemove觸發都會把從頭至尾把整個數組的點畫出來,因此須要擦除上次畫的線段,而後畫上新的線段,不然地圖上的線段將會重疊起來越積越多。這樣就能夠寫出以下代碼
map.addEventListener('mousemove',function(e){
//若是處於鼠標按下狀態,才能進行畫操做
if(isMouseDown){
//將鼠標移動過程當中採集到的路徑點加入數組保存
polyPointArray.push(e.point);
//除去上次的畫線
if(lastPolyLine) {
map.removeOverlay(lastPolyLine)
}
//根據已有的路徑數組構建畫出的折線
var polylineOverlay = new window.BMap.Polyline(polyPointArray,{
strokeColor:'#00ae66',
strokeOpacity:1,
enableClicking:false
});
//添加新的畫線到地圖上
map.addOverlay(polylineOverlay);
//更新上次畫線條
lastPolyLine = polylineOverlay
}
})
複製代碼
注意一個小細節,須要給Polyline參數設置一個enableClicking爲false的屬性,不然鼠標移到畫出的線段上時會顯示可點擊圖標,注意上述代碼並無處理刪除上次畫線的邏輯,這是放在map的mousedown事件裏處理
繼續分析,當鼠標擡起時代表畫線完成,此時地圖上會顯示一個有填充顏色的多邊形,這個怎麼處理?也很簡單,百度地圖提供了畫多邊形的api
map.addEventListener('mouseup',function(e){
//若是處於畫圈狀態下 且 鼠標是按下狀態
if(isInDrawing && isMouseDown){
//退出畫線狀態
isMouseDown = false;
//添加多邊形覆蓋物,設置爲禁止點擊
var polygon = new window.BMap.Polygon(polyPointArray,{
strokeColor:'#00ae66',
strokeOpacity:1,
fillColor:'#00ae66',
fillOpacity:0.3,
enableClicking:false
});
map.addOverlay(polygon);
//保存多邊形,用於後續刪除該多邊形
polygonAfterDraw = polygon
//計算房屋對於多邊形的包含狀況
var ret = caculateEstateContainedInPolygon(polygonAfterDraw);
//更新dom結構
ul.innerHTML = '';
var fragment = document.createDocumentFragment();
for(var i=0;i<ret.length;i++){
var li = document.createElement('li');
li.innerText ? li.innerText = ret[i] : li.textContent = ret[i];
fragment.appendChild(li);
}
ul.appendChild(fragment);
}
});
複製代碼
多邊形有各類參數能夠設置其樣式,畫出的多邊形可能很奇怪,由於你能夠亂畫,就像塗鴉同樣,下圖這種多邊形看似不合法其實也沒啥問題,中間能夠有各類洞,這裏面的具體邏輯就是百度api內部的事情了
這是本demo的第二個難點,網上一番查閱,發現一個叫射線法的方法比較好理解,以下圖所示
//斷定一個點是否包含在多邊形內
function isPointInPolygon(point,bound,pointArray){
//首先判斷該點是否在外包矩形內,若是不在直接返回false
if(!bound.containsPoint(point)){
return false;
}
//若是在外包矩形內則進一步判斷
//該點往右側發出的射線和矩形邊交點的數量,若爲奇數則在多邊形內,不然在外
var crossPointNum = 0;
for(var i=0;i<pointArray.length;i++){
//獲取2個相鄰的點
var p1 = pointArray[i];
var p2 = pointArray[(i+1)%pointArray.length];
//lng是經度,lat是緯度
//若是點座標相等直接返回true
if((p1.lng===point.lng && p1.lat===point.lat)||(p2.lng===point.lng && p2.lat===point.lat)){
return true
}
//若是point在2個點所在直線的下方則continue
if(point.lat < Math.min(p1.lat,p2.lat)){
continue;
}
//若是point在2個點所在直線的上方則continue
if(point.lat >= Math.max(p1.lat,p2.lat)){
continue;
}
//有相交狀況:2個點一上一下,計算交點
//特殊狀況2個點的橫座標相同
var crossPointLng;
//若是線段2個點x相同,則斜率無窮大,特殊處理
if(p1.lng === p2.lng){
crossPointLng = p1.lng;
}else{
//計算2個點的斜率
var k = (p2.lat - p1.lat)/(p2.lng - p1.lng);
//得出水平射線與這2個點造成的直線的交點的橫座標
crossPointLng = (point.lat - p1.lat)/k + p1.lng;
}
//若是crossPointLng的值大於point的橫座標則算交點(由於是右側相交)
if(crossPointLng > point.lng){
crossPointNum++;
}
}
//若是是奇數個交點則點在多邊形內
return crossPointNum%2===1
}
複製代碼
注意一個優化之處bound.containsPoint(point),首先判斷該點是否多邊形在外包矩形內,若是這個前提都不知足則直接pass, containsPoint是api提供的接口,能夠免去本身寫方法,因而可知要多讀api文檔,能夠減小工做量
注意這裏判斷直線位置關係的代碼,第一個if沒有等於,第二個有等號,這裏就實現了上述特例的判斷,這裏其實任意一個if有等號便可,還有要注意計算交點位置的代碼很容易寫錯,首先線段2個端點可算出斜率,而後交點和其中一個端點又可算出斜率,而交點的y已肯定,所以求出交點x值就垂手可得。
那麼如何判斷是右側的射線相交呢?很簡單,只需判斷交點的x值大於座標點的x值便可
//若是point在2個點所在直線的下方則continue
if(point.lat < Math.min(p1.lat,p2.lat)){
continue;
}
//若是point在2個點所在直線的上方則continue
if(point.lat >= Math.max(p1.lat,p2.lat)){
continue;
}
複製代碼
剩下的就是對地圖上全部點進行依次判斷便可,這裏的markerList是一個全局變量,在地圖初始化過程當中記錄了全部點的marker實例,這裏面getPath,getBounds等都是api接口,最後咱們返回全部marker上的label,即文本數組
//計算地圖上點的包含狀態
function caculateEstateContainedInPolygon(polygon){
//獲得多邊形的點數組
var pointArray = polygon.getPath();
//獲取多邊形的外包矩形
var bound = polygon.getBounds();
//在多邊形內的點的數組
var pointInPolygonArray = [];
//計算每一個點是否包含在該多邊形內
for(var i=0;i<markerList.length;i++){
//該marker的座標點
var markerPoint = markerList[i].getPosition();
if(isPointInPolygon(markerPoint,bound,pointArray)){
pointInPolygonArray.push(markerList[i])
}
}
var estateListAfterDrawing = pointInPolygonArray.map(function(item){
return item.getLabel().getContent()
})
return estateListAfterDrawing
}
複製代碼
至此,整個demo核心功能所有完成~~右側顯示出了3個被圈住的座標點
本demo的所有代碼放在github上,點這裏進入~~
項目過程當中遇到的一個坑:百度地圖的api不是準確無誤的,好比Label的api部分,當時我須要根據一個label實例獲得label的文本內容,此文檔只有setContent方法獲取文檔,沒有getContent,當時我就震驚了,這怎麼辦?若是獲取不到文本就無法作了,難道是百度地圖漏寫了?我在代碼中嘗試getContent()方法,果真!可以獲取到文本且沒報錯,因而可知文檔不是準確的,本身要多嘗試才能得出準確結果