我们经常使用地图查位置、看公交、看街景,同时地图还开放第三方的API给开发者。利用这些API进行地图的个性化的展示和控制,例如北京被水淹了,开发一个网页显示北京被淹的地图,地图上面标志被水淹的位置、严重程度,或者我是交警,想要在地图上标志发生车祸、被交通管制的路段,甚至是利用地图的街景,控制街景的位置变化做一个tour show动画。因为地图本身就是一个比较好玩的东西,再加上一些个性化的控制会更加的有趣。
常有的地图有谷歌、百度、必应等,这些都有提供api,下面以谷歌地图为例做说明。虽然谷歌被墙,但是谷歌有一个中国域名版本的,没有被墙,可以自由访问:http://www.google.cn/maps,估计很多人都不知道。首先来看下谷歌地图是怎么显示在页面的
谷歌地图的组成
只要做一下元素审查,就可以发现谷歌地图的主体部份是用一张张的图片拼成的,只要缩放比例或者位置一改变,就会再去请求新的图片,也就是说地图的渲染是在后端进行的,后端把图片生成好发给前端,之所以没放在前端绘制主要应该是考虑了客户端的性能和兼容性。左上角和右下角的控制也是用div absolute定位上去的。
谷歌地图的使用
引入谷歌地图
首先加载地图的api,你可以指定所用语言,如果没指定,地图将根据浏览器的语言(可通过请求的http头的Accept-Language字段)自动选用语言。还可以指定谷歌地图的版本,现在最新版是ver=3.25,还可以加上一些指定的地图的lib。必填的参数是key,如果没有key去谷歌地图的开发者页面申请一个即可。大陆版的跟正常版的在使用上目测没什么区别
<script src="http://ditu.google.cn/maps/api/js?key=AIzaSyBp&language=zh-CN"></script> <!-- 中国版 --> <!--正常版,需FQ <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBp"></script> -->
然后在页面写一个div,作为地图的容器,指定地图的宽高
<div id="map" style="width:100%;height:500px"></div>
初始化谷歌地图
var mapType = google.maps.MapTypeId.ROADMAP; var lat = 39.915168, lng = 116.403875, zoom = 10; var mapOptions = { center: new google.maps.LatLng(lat, lng), //地图的中心点 zoom: zoom, //地图缩放比例 mapTypeId: mapType, //指定地图展示类型:卫星图像、普通道路 scrollwheel: true //是否允许滚轮滑动进行缩放 }; var map = new google.maps.Map(document.getElementById("map"), mapOptions); //创建谷歌地图
这样在你的页面就有一个以天安门为中心的地图了
接下来,给天安门添加一个地理标志,使用谷歌自带的marker
添加一个Marker
通过上面代码的new我们已经有了个map对象,然后再创建一个marker对象,把这个marker绑定到map上
var marker = new google.maps.Marker({ map: map, position: new google.maps.LatLng(lat, lng) });
在地图上就可以看到天安门上标记了一个地理位置图标
接下来希望点一下这个marker的时候就显示这个位置的具体地址,如下所示,首先创建一个InfoWindow,并把它绑定到上面的那个marker的上方展示,同时给marker添加一个点击事件,一点的时候就打开提示框:
var infowindow = new google.maps.InfoWindow({content: "北京市天安门" }); //创建一个InfoWindow infowindow.open(map, marker); //把这个infoWindow绑定在选定的marker上面 //给这个marker添加点击事件 google.maps.event.addListener(marker, "click", function(){ infowindow.open(map,marker); });
效果如下:
这里所有的样式都是谷歌自带的,假设这个marker的样式跟网站的风格不太一致,我想要自定义一个marker不用谷歌自带的,那怎么办呢?在上面new一个marker的时候可以再传一个icon的参数,自定义icon,同时这个icon需要使用svg的格式。
在PSD里面将UI里面的icon形状导成一个AI文件,然后再用AI导出svg,就有了icon的svg格式。打开svg文件,将里面的path、fill等作为地图icon的参数,如下:
var locationMarker = { path: ‘M22 10.5c0 .895-.13 1.76-.35 2.588C20.025 20.723 13.137 28.032 11 28 9.05 28 3.2 21.28.926 14.71.334 13.42 0 11.997 0 10.5c0-.104.013-.206.017-.31C.014 10.117 0 10.04 0 9.967c-.005-.67.065-1.112.194-1.398C1.144 3.692 5.617 0 11 0c5.416 0 9.906 3.74 10.82 8.657.112.29.18.696.18 1.31 0 .083-.013.167-.015.25.003.095.015.188.015.283zM11 5.833c-2.705 0-4.898 2.09-4.898 4.667S8.295 15.167 11 15.167s4.898-2.09 4.898-4.667c0-2.578-2.193-4.667-4.898-4.667z‘, fillColor: ‘#E84643‘, fillOpacity: 1, strokeColor: ‘#E84643‘, }; var marker = new google.maps.Marker({map: map, icon: locationMarker, position: new google.maps.LatLng(lat, lng)});
就可以将默认的marker样式换掉,如下所示,你也可以换成其它各种各样的形状,像房子、车等icon
检查一下刚刚添加的marker,发现最后被谷歌地图转换成了一个canvas元素:
到这里已经可以解决在地图显示北京哪里被水淹了的问题。就是在被水淹的位置添加一个个的marker,现在我希望点击marker的时候能够显示该处的图文受灾情况。可以使用上面介绍的InfoWindow,只要把参数{content: "北京市天安门" },换成{content: "<div class=‘detail-info‘>...</div>"},然后写detail-info的样式即可。但是这样会有两个问题:
1. 不方便改变InfoWindow那个框的样式,例如没有一个直接的方法可以去掉右上角的x按钮
2. 假设有几百个地方被水淹了,也就是说得添加几百个marker,同时每个marker都得添加一个click事件,因为谷歌的事件没有marker事件委托,每个marker都得一个个加事件,一下子加几百个事件,这样就有点egg pain了。
所以说如果使用了上面的marker的方式,谷歌把它变成了一个canvas,以后所有的操作都得处处受制于谷歌的API,同时谷歌的API并不是十分的丰富和灵活。因此必须得另辟一条路,如果能够用原生的div放到谷歌地图里面那就简单多了,因为地图本身就是用div实现的,所以用原生的应该是可以的。其实只要用谷歌搜一搜就可以找到解决办法
使用原生html Marker
谷歌地图还提供了另外一个往地图里面加东西的OverlayView,使用这个的原理就是创建一个OverlayView对象然后给它append一个div,把这个div的position置为absolute(相对于地图的容器container),然后再设置它在这个容器的left/top位置,关键就在于怎样根据当前marker的经纬度转换为在容器的像素位置。而这个对象已经提供了一个转换方法可以调用。将自定义的Marker封装成一个类,例如现在要做一个房源的地图展示,需要把房源标在地图上,自定义一个HouseMarker的类:
function HouseMarker(latlng, map, args) { this.latlng = latlng; //for google map this.setMap(map); //for google map this.args = args; //自定义参数 }
再将这个HouseMarker的原型指向谷歌的OverlayView进行继承
HouseMarker.prototype = new google.maps.OverlayView();
然后实现这个原型的draw函数:
HouseMarker.prototype.draw = function() { //创建一个div $div = $("<div class=‘marker-container‘><div class="marker"></div><div class=‘detail-info‘ style="display:none"></div></div>"); //将div添加到它的dom元素里面 var panes = this.getPanes(); var div = $div[0]; panes.overlayImage.appendChild(div); //计算经纬度计算div的像素位置 var point = this.getProjection().fromLatLngToDivPixel(this.latlng); div.style.left = (point.x - 20) + ‘px‘; //减掉marker宽度的一半,居中 div.style.top = (point.y - 20) + ‘px‘; //减掉marker高度的一半,居中 };
再调new HouseMarker,传进当前的经纬度和map对象,就可以在地图正确的位置上显示这个marker了。接下来就能够使用原生的js事件和css控制这个marker了,这样就很方便灵活了。特别是谷歌的mouse事件,即使是上一个marker盖住了下面的marker,鼠标移到上面那个marker时,仍然会触发下面那个marker的事件,这样就有点恶心了。而使用原生的mouse事件就没有这种情况。
如下面所示,鼠标hover的时候就显示详细信息,如果这个详细信息刚好下面有个marker就会出现上面讨论的情况:
详见:Custom HTML Markers with Google Maps
我们已经初步解决了marker展示的问题,但其实还有一些问题:展示这些详细信息会出现超出可见区域的情况
边界判断
当这个marker比较靠边的时候,详情的框会超出显示范围:
所以需要做边界判断,不管marker在什么位置,详情框都可以在展示区域内显示,效果如下:
也就是说需要判断当前marker是否超出了地图容器能够正常显示的范围,如果超出了就要做下处理——如果太靠上就把详情展示在下面,如果太靠右详情框就不应该是和marker水平居中了,而是要往左移一移,同时把三角形的位置挪一挪。所以关键是要做一个边界判断,而做边界判断的前提是拿到marker在容器里面的left/top位置。
已经不可以再上使用上面获取位置的方法了,因为那个位置算好之后不会再变,不会跟着地图的拖动而发化变化,谷歌地图是借助transform等设置改变它的位置,而不是用position了。
但是可以拿到当前地图在这个容器里面的边界经纬度,最东、最西、最北、最南,也可以拿到这个容器的像素宽高,所以就可以知道一个像素对应地图多少经纬度,即像素/经纬度的比例ratio。同时marker的纬度是知道的,可以算一下它距离边界的经纬度dx, dy,dx除以ratio就能够换算像素值了。代码如下:
var mapBounds = mapHandler.getBounds(); //调用谷歌的api获取容器经纬度边界并做一些处理 var xRatio = (mapBounds[1] - mapBounds[0]) / mapWidth, yRatio = (mapBounds[3] - mapBounds[2]) / mapHeight; //marker的经纬度 var lat = marker.latlng.lat(), lng = marker.latlng.lng(); //转换marker的像素位置 var pos = { top: -(+lat - mapBounds[3]) / yRatio, left: (+lng - mapBounds[0]) / xRatio, bottom: (+lat - mapBounds[2]) / yRatio, right: -(+lng - mapBounds[1]) / xRatio }; var posFlag = 0, maxLen = 150, maxLeftLen = 118; //右边超出 if(pos.right < maxLen) posFlag |= 1; //上面超出 if(pos.top < maxLen) posFlag |= 2; //左边超出 if(pos.left < maxLeftLen) posFlag |= 4; //对超出的情况进行处理,代码略 switch(posFlag){ case 1: //右 case 2: //上 case 3: //右上 case 4: //左 case 6: //左上 }
还有一种情况是如果详情框太长了,超出了容器的一半,不管向上显示还是向下显示,marker刚好在正中间时,详情框都会超出显示范围。这种情况可以借助第二种解决办法,就是将地图移动一下,超出的就可以显示了。需要计算移动后的地图中心点在哪里,再调API提供的panTo就可以了,如下。难点是计算要正确
绘制形状
接下来再简单讨论一个高级话题,就是在谷歌地图上面绘制一个形状,然后获取该形状的地理位置。谷歌已经提供了一个叫DrawingManager的类,只要new一个对象,传些参数,就可以在地图上显示draw tool了,如下:
然后再监听这个manager的complete事件,在complete事件里面获取当前画的图形的范围,例如上面的圆可以获取到它的圆心和半径。详见:Drawing Layer (Library)
然而谷歌提供的这个工具非常的简陋,你无法直接改变上面工具栏的icon,就连画的圆也不是边界也是扭扭曲曲的,如上所示。可用事件只提供完成事件,没有画时候的事件,所以你没办法在画的时候加一个不断变化的、显示所画范围多少公里的提示框。
因此另外一个解决办法是自已实现一个类似的工具,通过鼠标的mousedown、mousemove、mouseup事件搭配组合,结合上面画marker的方法,再使用svg,动态改变它的path做到实时变化的效果。这样就很灵活了,想怎么搞就怎么搞,但是代码量应该也是挺大的。
除此之外还有街景、3D控制的API,这里不再讨论,有兴趣自己查查谷歌的API。