我們經常使用地圖查位置、看公交、看街景,同時地圖還開放第三方的API給開發者。利用這些API進行地圖的個性化的展示和控制,例如北京被水淹了,開發一個網頁顯示北京被淹的地圖,地圖上面標誌被水淹的位置、嚴重程度,或者我是交警,想要在地圖上標誌發生車禍、被交通管制的路段,甚至是利用地圖的街景,控制街景的位 ...
我們經常使用地圖查位置、看公交、看街景,同時地圖還開放第三方的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,把marker和詳情框寫在一起,方便後面的展示和隱藏 $div = $("<div class='marker-container'>" +
" <div class="marker"></div>" +
" <div class='detail-info'">" +
"</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事件就沒有這種情況。其實這個也是可以理解的,因為谷歌地圖是用的一個canvas畫布展示marker,在這個畫布裡面只根據滑鼠的位置和marker的位置判斷滑鼠有沒有進入marker裡面,所以不管上面有沒有被蓋住,只要算出來的位置是符合的。
如下麵所示,滑鼠hover的時候就顯示詳細信息,如果這個詳細信息剛好下麵有個marker就會出現上面討論的情況:
詳見:Custom HTML Markers with Google Maps
第二步是的滑鼠hover的時候展示詳情框,最簡單的就是用CSS控制即可,使用上面定義的DOM結構,初始化時讓detail-info隱藏:
.marker-container .detail-info{ display: none }
然後再設置:
.marker-container:hover .detail-info{ display: block }
就可以了,不用一行JS
第二種辦法是監聽mouse事件,使用事件委托:
$("#map").on("mouseover", ".marker-container", function(){ $(this).find(".detail-info").show(); }); $("#map").on("mouseout", ".marker-container", function(){ $(this).find(".detail-info").hide(); });
用JS的進行顯示和隱藏的好處是:可以對展示做一些後續的處理,這也是下麵要提到的
我們已經初步解決了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。