原生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()方法,果真!可以获取到文本且没报错,因而可知文档不是准确的,本身要多尝试才能得出准确结果

相关文章
相关标签/搜索