Google地圖開發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>");
    //將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。