使用 Google Maps API (v3) 中的 Image Map Type

使用 ImageMapType 讓你的 Maps 更有自己的風格!

前情提要

因為義務幫忙今年的 COSCUP 製作了一個 webapp 供參加者來使用,目前已經接近完成(因為有別的事在忙,所以拖了有點久)的階段,過幾天會正式放出來給大家使用,不過我先寫一些製作的心得分享文。

什麼是 ImageMapType

在 COSCUP Mobile WebApp 中的「Maps」部份,為了顯示出活動地點的室內平面圖,這個東西是我參考 Google I/O 2010 提供的會場地圖的方式而製作出來,使用自己繪製的平面圖疊加在原本的衛星圖資之上,它的顯示效果如下所示:

如果你曾經有注意過 Google 與電影「赤壁」合作過的活動網頁,你會發現其中有一個呈現赤壁古戰場風貌的地圖小工具(Mapplet),而它也是利用了 ImageMapType 的方式將古地圖貼上去。若是你也有類似的開發需求,不必抓破頭煩惱怎麼把自己的圖片疊上去而寫出複雜的 JavaScript 程式碼,只需要利用 Google Maps JavaScript API v3 中的 ImageMapType 部份的 API 就可以完成,只不過在實務上該怎麼進行,以及要注意的事項在目前的官方文件並沒有寫得很清楚,這篇文章將會介紹我是如何完成這個效果,以及開發時的心得。

Google地圖圖資規格

Google 地圖的圖資是以一堆正方形的 tiles 所組成,每一個 tile 的大小是 256px * 256px(不論縮放,不同縮放大小就是不同的 tile,但大小都是 256px * 256px),而每個 tile 有 z, x, y 三個屬性,z 是表示縮放大小,而 x, y 則是 tile 在全球圖資中的 x, y 座標(與經緯度的值相關,但座標系統不一樣)。以中研院人文資訊大樓為例,若在縮放等級18時,圖資的 tiles 是這樣排列的:

這個示意圖我以紅線作為 tiles 的分界(只是畫個大概,不用真的去量是不是 256px * 256px),而淺藍色的座標數字則表示該 tile 的z, x 及 y 值。如果你要利用 Google Maps API 中的 ImageMap Overlay 來疊上自己的圖片,你必須「做出覆蓋原先 tiles 的 tiles」才能蓋上去,也就是在這個縮放等級之下,我至少要自行製作兩個 tiles 才能把目標物給覆蓋住,最後的成果便是這兩張圖(僅3樓的部份,注意空白的部份其實是透明的):

為了方便起見,我將這兩張圖的檔案名稱根據 Google Maps 圖資的座標來命名為「t_18_219626_112227.png」及「t_18_219626_112228.png」,數字分別對應到 z, x, 及 y,至於為什麼要這樣做,接下來就要介紹 ImageMap Overlay 的使用作法。

使用ImageMapType API

API的詳細使用方法可以參考 Google Maps API 的文件,我這裡大概說明一下程式碼大概需要怎麼寫。在你建立好一個 mapInstance 後,使用 ImageMap 的程式嗎如下:


// ImageMap Overlay 的選項
var imgMapTypeOpt = {
‘getTileUrl’: function(coord, zoom) {
var url = [‘http://tiles.example.com/t_’,
zoom, ‘_’, coord.x, ‘_’, coord.y, ‘.png’].join(‘’);
return url;
},
‘isPng’: true
};
// 建立 ImageMap Overlay type
var imgOverlay = new google.maps.ImageMapType(imgMapTypeOpt);
// 疊在 Map 上,mapInst 是前面建立地圖時 new google.maps.Map 的物件
mapInst.overlayMapTypes.insertAt(0, imgOverlay);

一旦在 map 上加上 ImageMapType 之後,當 Google Maps API 把原本的該顯示的圖資(也就是你看得到的範圍)顯示完畢後,接著會根據每一塊顯示出來的 tile(注意這段話,等一下會發生問題),藉著你提供的 getTileUrl 這個 callback function 來找出期望疊加在原有 tile 上的圖片,也就是說,當 Google Maps API 在顯示 (18, 219626, 112227) 這塊 tile 時,根據這段程式碼的設計,它會試著去讀取 http://tiles.example.com/t_18_219626_112227.png 這張圖片,因為它在呼叫 getTileUrl 這個 callback function 時就得到了這個回傳值,這就是上面提到,為什麼把圖片的檔名對應到座標,如此一來才方便你寫 callback function。這樣一來,Google Maps API 只要讀取到對應座標的圖片,就會自動疊加上去了。

產生的問題以及解決方案

假設你已讀懂上一段的敘述,接下來就會產生幾個問題:

  1. Maps API 會依照「可視範圍」來尋找 image overlay 的位置,這在大多數的狀況是完全沒有必要的。因為以我做中研院大樓的例子而言,在縮放等級18時我只需要疊加兩張 tiles ,但是我的可視範圍可能是好多塊 tiles,這就多花了很多時間在讀取空的資料(例如從檔案伺服器那裡要到了一堆 404 的回應),除了發出了沒必要的 http requests 之外(也暴力地打你的檔案伺服器),因為這個 API 是在 client 端執行的,也會拖慢 client 端瀏覽器的效能。

    所幸當 getTileUrl 的回傳值若為空字串時,Maps API 就會判斷該座標不需要去抓圖,所以只要輕鬆設定一個座標範圍的檢查就可以解決發出過多 requests 的問題,像是這樣:


    getTileUrl: function(coord, zoom) {
    ....
    if (coord.x < BOUND.west || coord.x > BOUND.east ||
    coord.y < BOUND.north || coord.y > BOUND.south) {
    return “”;
    } else {
    return url;
    }
    }

    這樣的簡單的判斷法雖然不保證 100% 避免浪費的 requests,但至少已經縮減了很多 trivial 的不必要,若是你的 image 分佈得不是很均勻,可就要好好複習一下幾何演算法,有效地判斷什麼時候才該回傳圖檔、什麼時候回傳空字串。

  2. 在上面的例子中,我只示範了在縮放等級18的顯示效果,事實上我做了縮放等級17, 18及19的效果,所以我必須依照不同縮放等級而準備不同的 tiles(實際數據是,單以3樓為例,z=17 有2張,z=18有2張,z=19有5張,所以兩層樓都做下去就要準備18張圖),既然我已經確定我只做這三個縮放層級,那我可以在 imageMapTypeOpt 選項中加入兩個屬性,用來限制 maps api 不要在其它縮放層級還去呼叫 getTileUrl:

    var imgMapTypeOpt = {
    ....
    minZoom: 17,
    maxZoom: 19
    };

  3. 另外就是我雖然切換了兩層樓,但在地球上這兩層樓是同一個經緯度座標呀(廢話!)!所以對應到的 Google Maps 圖資 tiles 也是一樣,這裡就要在另外從檔名及 JavaScript 來下手,細節我就不贅述了,不過重點是:如果要更換 ImageMapType 的話,記得要先將原本的疊加層先移除,然後再加入新的才行(產生過的 imageMapType 記得存下來,不要每次都 new 新的):


    if (mapInst.overlayMapTypes.length != 0) {
    mapInst.overlayMapTypes.removeAt(0);
    }
    // 這裡再加另一個 imageMapType

  4. 到現在我們都瞭解 Google 圖資的 tiles 有座標了,那我要怎麼知道它的座標到底是啥啊!而且又要怎麼知道到底 Google 圖資的 tiles 是怎麼切割的?身為一個網頁前端工程師,我不相信你不會使用 Firefox + firebug 或是 Chrome + devtools 來「分析」一個頁面在載入的過程中到底讀取了什麼東西........嗯,剩下的就看你是不是一個合格的前端工程師囉 😛

結論

經過我這樣隨意地解釋之後,想必你也瞭解使用 ImageMapType 並不會要寫多麼複雜的程式,因為所有的苦工都是在處理圖片、對準 tiles 啊! XD 如果你本身就很熟悉影像工具的話應該會減輕這方面的負擔。

最後希望大家若是有使用 Google Maps API 介接圖資的用途時,不妨考慮做出更多具有自己風格的 ImageMap Overlay 吧!