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

超級索尼發表於2018-05-26

概述

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

鏈家網畫圈找房效果

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
下面是專案中實現的畫圈找房,可以看出效果和鏈家網很相似

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

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

為什麼寫這篇文章

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

Step 0: 準備工作

此demo未採用框架,用原生js實現,專案裡是用react技術棧實現,開啟你最喜歡的IDE,新建如下3個檔案draw.js, draw.css, draw.html, 我這裡是用webstorm編輯程式碼,demo結構如下圖

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
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;
}
複製程式碼

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

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

Step 1:百度地圖初始化

本demo需要地圖的支援,這裡採用百度地圖,騰訊地圖和高德地圖也應該可以實現本demo的效果,首先登入百度地圖開放平臺進行賬號註冊,如果已有百度賬號可以不用註冊
在使用百度地圖服務前需要申請金鑰(ak), 點選 開發文件 -> JavaScript API進入Javascript指南部分,按照指南註冊好自己的金鑰(ak)

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
申請好的金鑰如下所示

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

然後在新增百度地圖指令碼到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);              // 開啟滑鼠滾輪縮放
}
複製程式碼

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
至此,百度地圖初始化完成~~~

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)
    })
}
複製程式碼
重新整理頁面發現地圖上已經有了點資料和標註

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
然後我們需要設定一些全域性變數儲存畫圈邏輯的相關資料,後面會解釋各個變數的作用

/*** 介面元素 ***/
//畫圈按鈕
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如下所示:

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
其中最符合需求的就是一個畫出矩形或者圓,但是這樣也達不到鏈家那種很連貫隨意畫圖的效果,怎麼辦?開始也很費解,仔細研究鏈家網的畫圈,放大地圖進行畫圖觀察,發現看似很連貫畫出的圖在放大狀態下是由折線段組成,這就說明了這其實是用線段模擬畫圈的操作

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

回去繼續查閱百度地圖api,發現有在地圖上畫出折線的api,其引數是一個由Point組成的陣列,只要給出這個陣列就能畫出折線來,點選這裡前往百度地圖api

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
因此逐漸有了眉目,那就是需要獲取這樣一個由不同Point組成的陣列然後呼叫該api就能在地圖上畫圖了,那麼如何獲取陣列呢?因為圖時在滑鼠按下且移動過程中畫出來的,所以肯定是在map的mousemove事件上做文章,我猜想mousemove的回撥函式中能夠獲取滑鼠在地圖上的座標點,然後繼續查閱相關api,果然!驗證了我的猜想

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
如下程式碼便能夠獲取到滑鼠移動過程中的每一個點的座標

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

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
其引數恰好也是一個由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內部的事情了

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

程式碼後半部分caculateEstateContainedInPolygon方法會計算出地圖上哪些點包含在所畫出的圈內,然後會更新右側列表重新整理資料顯示,下一節詳細介紹

Step 4: 如何判斷點在任意多邊形內

這是本demo的第二個難點,網上一番查閱,發現一個叫射線法的方法比較好理解,如下圖所示

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
原理就是地圖上每個點往右側發出一條射線,然後計算該射線與多邊形邊交點的個數
奇數個: 比如c,e,那麼點就在多邊形內部
偶數個: 比如a,b,那麼點就在多邊形外部
不過有一種特例,如果點在內部且與多邊形的交點恰好在2個線段的交點上,比如X點,該點在多邊形內部,但是該點與多邊形2個邊都有交點,只不過重合了,所以要特殊處理,對於這種特例,可採取如下辦法解決

原生Js從0開始實現一個鏈家網地圖畫圈找房功能
如上圖,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個被圈住的座標點

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



結語

本demo的全部程式碼放在github上,點這裡進入~~

專案過程中遇到的一個坑:百度地圖的api不是準確無誤的,比如Label的api部分,當時我需要根據一個label例項得到label的文字內容,此文件只有setContent方法獲取文件,沒有getContent,當時我就震驚了,這怎麼辦?如果獲取不到文字就沒法做了,難道是百度地圖漏寫了?我在程式碼中嘗試getContent()方法,果然!能夠獲取到文字且沒報錯,由此可見文件不是準確的,自己要多嘗試才能得出準確結果

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

相關文章