原生Js從0開始實現一個鏈家網地圖畫圈找房功能

概述

最近作項目的時候遇到的一個需求:要實現一個鏈家網地圖找房中的畫圈找房功能。鏈家網是採用百度地圖實現房源展現,先來看看鏈家網的畫圈找房功能,有木有很炫酷~~,能夠到鏈家上體驗一下

鏈家網畫圈找房效果 javascript

下面是項目中實現的畫圈找房,能夠看出效果和鏈家網很類似

項目中畫圈找房效果

下面就來手把手從0開始實現一個畫圈找房的demo~~ Js代碼一共200行左右,很輕量

爲何寫這篇文章

主要是想分享下在完成這個畫圈找房功能的過程當中,面對沒有現成api調用或者方案的問題,本身的思路過程以及遇到的一些問題是怎麼解決的

css

Step 0: 準備工做

此demo未採用框架,用原生js實現,項目裏是用react技術棧實現,打開你最喜歡的IDE,新建以下3個文件draw.js, draw.css, draw.html, 我這裏是用webstorm編輯代碼,demo結構以下圖 html

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;
}
複製代碼

上述實現了一個左側自適應右側固定寬度的佈局,左側容器用於展現百度地圖,右側是操做面板,頁面以下圖所示,點擊畫圈找房進入畫圈狀態,點擊退出畫圈按鈕進入正常操做地圖狀態, 按鈕下面是顯示數據列表部分java


Step 1:百度地圖初始化

本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);              // 開啓鼠標滾輪縮放
}
複製代碼

至此,百度地圖初始化完成~~~

Step 2: 事件綁定和點數據放置

咱們須要在地圖上放置幾個點做爲畫圈的初始數據,查閱百度地圖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;
})
複製代碼

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

其參數剛好也是一個由Point組成的數組,這個數組就是上述畫線的數組,如出一轍。所以當鼠標擡起時,擦除上次畫的線,而後再根據polyPointArray繪製一個多邊形不就畫出了整個圈了麼!map的mouseup代碼以下

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()方法,果真!可以獲取到文本且沒報錯,因而可知文檔不是準確的,本身要多嘗試才能得出準確結果

相關文章
相關標籤/搜索