最近作项目的时候遇到的一个需求:要实现一个链家网地图找房中的画圈找房功能。链家网是采用百度地图实现房源展现,先来看看链家网的画圈找房功能,有木有很炫酷~~,能够到链家上体验一下
链家网画圈找房效果 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()方法,果真!可以获取到文本且没报错,因而可知文档不是准确的,本身要多尝试才能得出准确结果