概述
最近做專案的時候遇到的一個需求:要實現一個鏈家網地圖找房中的畫圈找房功能。鏈家網是採用百度地圖實現房源展示,先來看看鏈家網的畫圈找房功能,有木有很炫酷~~,可以到鏈家上體驗一下
鏈家網畫圈找房效果
專案中畫圈找房效果
下面就來手把手從0開始實現一個畫圈找房的demo~~ Js程式碼一共200行左右,很輕量
為什麼寫這篇文章
主要是想分享下在完成這個畫圈找房功能的過程中,面對沒有現成api呼叫或者方案的問題,自己的思路過程以及遇到的一些問題是怎麼解決的
Step 0: 準備工作
此demo未採用框架,用原生js實現,專案裡是用react技術棧實現,開啟你最喜歡的IDE,新建如下3個檔案draw.js, draw.css, draw.html, 我這裡是用webstorm編輯程式碼,demo結構如下圖
draw.html,draw.css程式碼如下,js檔案暫時為空,<!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;
}
複製程式碼
上述實現了一個左側自適應右側固定寬度的佈局,左側容器用於展示百度地圖,右側是操作皮膚,頁面如下圖所示,點選畫圈找房進入畫圈狀態,點選退出畫圈按鈕進入正常操作地圖狀態, 按鈕下面是顯示資料列表部分
Step 1:百度地圖初始化
本demo需要地圖的支援,這裡採用百度地圖,騰訊地圖和高德地圖也應該可以實現本demo的效果,首先登入百度地圖開放平臺進行賬號註冊,如果已有百度賬號可以不用註冊
在使用百度地圖服務前需要申請金鑰(ak), 點選 開發文件 -> JavaScript API進入Javascript指南部分,按照指南註冊好自己的金鑰(ak)
然後在新增百度地圖指令碼到draw.html中,然後把ak後面的中文換成剛剛申請的金鑰即可
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=您的金鑰">
複製程式碼
最後在draw.js裡寫下如下程式碼,百度地圖初始化就完了成了,執行draw.html可以看到地圖已經展示出來了(見下圖)!第一句程式碼裡的container是地圖容器的id,我們選擇北京作為展示座標
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); // 開啟滑鼠滾輪縮放
}
複製程式碼
至此,百度地圖初始化完成~~~
Step 2: 事件繫結和點資料放置
我們需要在地圖上放置幾個點作為畫圈的初始資料,查閱百度地圖API,寫下在draw.js裡寫如下函式進行初始化,並在window.onload中呼叫該方法
//初始化地圖座標點
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的基本邏輯如下:
點選畫圈找房按鈕進入畫圈狀態,在畫圈狀態下,在地圖上按下滑鼠左鍵開始畫圈操作,移動滑鼠進行畫圈操作,然後抬起滑鼠左鍵完成畫圈,此時地圖上會顯示出圈,然後右列表會顯示出圈內座標的文字。最後點選退出畫圈按鈕退出畫圈狀態
因此需要為地圖以及按鈕繫結事件,寫下如下函式進行事件繫結:
//開始畫圈繫結事件
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;
})
複製程式碼
Step 3: 如何實現‘畫’操作
這是本demo的第一個難點,如何實現鏈家的這種類似畫筆的畫操作?我翻看了百度地圖關於畫圖的所有api後,只發現百度地圖提供了繪製圓,直線,多邊形,矩形的api,官網demo如下所示:
其中最符合需求的就是一個畫出矩形或者圓,但是這樣也達不到鏈家那種很連貫隨意畫圖的效果,怎麼辦?開始也很費解,仔細研究鏈家網的畫圈,放大地圖進行畫圖觀察,發現看似很連貫畫出的圖在放大狀態下是由折線段組成,這就說明了這其實是用線段模擬畫圈的操作回去繼續查閱百度地圖api,發現有在地圖上畫出折線的api,其引數是一個由Point組成的陣列,只要給出這個陣列就能畫出折線來,點選這裡前往百度地圖api
因此逐漸有了眉目,那就是需要獲取這樣一個由不同Point組成的陣列然後呼叫該api就能在地圖上畫圖了,那麼如何獲取陣列呢?因為圖時在滑鼠按下且移動過程中畫出來的,所以肯定是在map的mousemove事件上做文章,我猜想mousemove的回撥函式中能夠獲取滑鼠在地圖上的座標點,然後繼續查閱相關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內部的事情了
程式碼後半部分caculateEstateContainedInPolygon方法會計算出地圖上哪些點包含在所畫出的圈內,然後會更新右側列表重新整理資料顯示,下一節詳細介紹
Step 4: 如何判斷點在任意多邊形內
這是本demo的第二個難點,網上一番查閱,發現一個叫射線法的方法比較好理解,如下圖所示
原理就是地圖上每個點往右側發出一條射線,然後計算該射線與多邊形邊交點的個數奇數個: 比如c,e,那麼點就在多邊形內部
偶數個: 比如a,b,那麼點就在多邊形外部
不過有一種特例,如果點在內部且與多邊形的交點恰好在2個線段的交點上,比如X點,該點在多邊形內部,但是該點與多邊形2個邊都有交點,只不過重合了,所以要特殊處理,對於這種特例,可採取如下辦法解決 如上圖,x,y,z都是特例點,x,z在多邊形外,y在多邊形內,按之前的思路y是有2個交點,而x也有2個交點,但是實際是一內一外,所以我們需要重新定義交點的含義:我們規定當交點所線上段的2個點都在交點以上,該交點能算一個交點。這樣一來,對於y,交點在c,cd都在y點上面,因此算一個交點,而cb的b點在y下面,因此不算交點,所以y點最終只有一個交點。同理x交點為0個,z交點為2個。這個方法落實到程式碼裡也很簡單,下面就是上述思路的實現
//判定一個點是否包含在多邊形內
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()方法,果然!能夠獲取到文字且沒報錯,由此可見文件不是準確的,自己要多嘗試才能得出準確結果