將地址轉換成地理座標的程序被稱為地理編碼(Geocoding),Google Maps API亦支援地理編碼服務(注意: 有每天查詢次數不可超過2,500次的限制,申請Google Maps API Premier可以提高到100,000次),呼叫方法很簡單,使用URL "http: //maps.googleapis.com/maps/api/geocode/json?address=要轉換的地址&sensor=ture或false",便可得到一份JSON格式的地址座標資訊,address參數除了完整地址,也可以輸入一般性地名或片段地址,另外也能指定傳回XML格式、使用語系、檢視範圍(優先列出該範圍內符合結果,最經典的例子是在台灣查詢"中正路"、"中山路"之類的菜市場路名,要指定縣市範圍才能找對目標)... 等等,完整參數說明可參見API文件

先用瀏覽器來個簡單測試,在網址輸入http://maps.googleapis.com/maps/api/geocode/json?address=%E5%8F%B0%E5%8C%97%E5%B7%BF%E5%85%89%E5%BE%A9%E5%8D%97%E8%B7%AF100%E8%99%9F&sensor=false (地址中文部分經encodeURI()編碼),會得到以下結果:

排版顯示純文字
{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "100",
               "short_name" : "100",
               "types" : [ "street_number" ]
            },
            {
               "long_name" : "光復南路",
               "short_name" : "光復南路",
               "types" : [ "route" ]
            },
            {
               "long_name" : "華聲里",
               "short_name" : "華聲里",
               "types" : [ "sublocality", "political" ]
            },
            {
               "long_name" : "大安區",
               "short_name" : "大安區",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "台北市",
               "short_name" : "北市",
               "types" : [ "administrative_area_level_2", "political" ]
            },
            {
               "long_name" : "台灣",
               "short_name" : "TW",
               "types" : [ "country", "political" ]
            },
            {
               "long_name" : "106",
               "short_name" : "106",
               "types" : [ "postal_code" ]
            }
         ],
         "formatted_address" : "106台灣台北市大安區光復南路100號",
         "geometry" : {
            "location" : {
               "lat" : 25.0438660,
               "lng" : 121.5563610
            },
            "location_type" : "ROOFTOP",
            "viewport" : {
               "northeast" : {
                  "lat" : 25.04521498029150,
                  "lng" : 121.5577099802915
               },
               "southwest" : {
                  "lat" : 25.04251701970849,
                  "lng" : 121.5550120197085
               }
            }
         },
         "partial_match" : true,
         "types" : [ "street_address" ]
      }
   ],
   "status" : "OK"
}

Google Map很貼心地將地址拆解成國家、城市、行政區、路名、門牌,同時geometry中即為我們所要的經緯度座標值,另外也包含建議的地圖檢視範圍。

雖然結果是JSON格式,但因為它是第三方網站且未支援JSONP整合,基於XMLHttpRequest不能跨網站呼叫的限制,我們無法直接在網頁JavaScript中用$.ajax()之類的方式直接查詢,我的解法是寫一個極簡單的ashx當成Proxy來克服: [2012-06-19更新: Google Maps API另有提供從Javascript端進行地理編碼的做法,可以全部在Client端處理完成,不需動用ashx,謝謝Ammon大人補充!]

排版顯示純文字
<%@ WebHandler Language="C#" Class="Geocode" %>
 
using System;
using System.Web;
 
public class Geocode : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        //注意: 如要防止被未授權對象將本程式當跳板對Google Maps API查詢,
        //      減損每日2,500次的額度,可再加入呼叫端檢核,此處省略以求單純
        var wc = new System.Net.WebClient();
        string address = context.Request["address"];
        context.Response.BinaryWrite(
            wc.DownloadData(
            "http://maps.googleapis.com/maps/api/geocode/json?address="
            + HttpUtility.UrlEncode(address) + "&sensor=false&language=zh-TW")
        );
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }
 
}

如此即可透過$.post("Geocode.ashx", { address: "查詢地址" }, function(result) { ... }, "json");的方式進行地址轉換,以下是一個應用範例,依據事件做好的地址清單,在地圖顯示台北市各消防分隊的位置:

排版顯示純文字
<!DOCTYPE html>
<html>
<head runat="server">
    <title>Geocoding Test</title>
    <script type='text/javascript' 
            src='http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.js'></script>   
    <script src="https://maps.google.com/maps/api/js?sensor=true"></script>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <style>
        body,input { font-size: 9pt; }
        html { height: 100% }  
        body { height: 100%; margin: 0px; padding: 0px }  
        #map_canvas { height: 100% }        
    </style>
    <script>
        $(function () {
            var markers = [];
            //呼叫ashx, 透過Google地理編碼將地址轉為經緯座標, 並傳回$.ajax() deferred
            function geocodeAjax(name, addr) {
                var ajax =
                        $.post("Geocode.ashx", { address: addr }, 
                        function (r) {
                            if (r.status == "OK") {
                                var loc = r.results[0].geometry.location;
                                markers.push({
                                    title: name + "@" + addr,
                                    latlng: new google.maps.LatLng(loc.lat, loc.lng)
                                });
                            }
                        }, "json");
                return ajax;
            }
 
            $.get("AddrList.txt", {}, function (list) {
                var lines = list.replace(/\r/g, "").split('\n');
                var deferredArray = [];
                //lines[i]格式如下:
                //中正中隊-華山分隊,(02)23412668,中正區北平東路1號 
                for (var i = 0; i < lines.length; i++) { //lines.length; i++) {
                    var parts = lines[i].split(',');
                    //以AJAX進行地址轉換,並將$.ajax() deferred物件放入陣列中
                    deferredArray.push(geocodeAjax(parts[0], parts[2]));
                }
                //利用jQuery deferred特性,在所有地址轉換AJAX呼叫完畢後,執行特定動作
                $.when.apply(null, deferredArray).then(function () {
                    //設定地圖參數
                    var mapOptions = {
                        mapTypeId: google.maps.MapTypeId.ROADMAP //正常2D道路模式
                    };
                    //在指定DOM元素中嵌入地圖
                    var map = new google.maps.Map(
                        document.getElementById("map_canvas"), mapOptions);
                    //使用LatLngBounds統計檢視範圍
                    var bounds = new google.maps.LatLngBounds();
                    //加入標示點(Marker)
                    for (var i = 0; i < markers.length; i++) {
                        var m = markers[i];
                        //將此座標納入檢視範圍
                        bounds.extend(m.latlng);
                        var marker = new google.maps.Marker({
                            position: m.latlng,
                            title: m.title,
                            map: map
                        });
                    }
                    //調整檢視範圍
                    map.fitBounds(bounds);
                });
            });
        });
    </script>
</head>
<body>
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

程式不長,但有幾個小地方值得補充:

  1. 消防分局地址被存在CSV檔案中,用$.get()方式取回解析。
  2. 由於每一個地址會觸發一次$.post()非同步呼叫,而地圖顯示要在所有非同步呼叫都取回結果後才執行。因此可善用jQuery 1.5+在$.ajax()加入的Deferred物件概念,利用$.when().then()等待所有$.ajax()呼叫都完成後才執行顯示地圖的邏輯。
  3. 由於$.when()的語法為$.when(ajax1, ajax2, ajax3),而我們的$.ajax()物件是保存在陣列中,難以一一列舉成參數,此時apply()就派上用場囉! 透過$.when.apply(null, ajaxArray).then(...)的寫法,可達到跟$.when(ajax1, ajax2, ajax3, …)完全相同的結果。[補充: $.when.apply() 第一個參數該傳$還是傳null?]
  4. 為了讓地圖縮放到能將所有標示點都包含在顯示範圍內,可使用map.fitBounds()方法,傳入參數為google.maps.LatLngBounds()物件。原本我還自己逐一比對每一個座標的經緯度統計東西南北邊界,後來才發現直接用LatLngBounds.extend(LatLng)就可以囉~ 真是貼心。

執行結果如下:


Comments

# by 宅男嘿嘿

我記得一次要地址json資料不能超過10筆 原來還可以一個地址Ajax叫一次標記地圖一次 就可以達成標記點出現在地圖上10個點以上 學習了

# by Jeffrey

to Ammon, 原來有Client版,謝謝補充! 已在本文加註說明。

# by chester

你好.請問不用ashx,使用Ammon的方法,應該怎麼寫?

# by chester

我不是很懂jquery,能指導下嗎?不用ashx,使用Ammon的方法,應該怎麼寫?能不能再寫一篇博文呢.

# by mick

這樣子下就可以直接取得經緯度了 http://maps.google.com/maps/geo?output=csv&oe=utf-8&q=%E5%8F%B0%E5%8C%97%E5%B8%82%E9%87%8D%E6%85%B6%E5%8D%97%E8%B7%AF%E4%B8%80%E6%AE%B5122%E8%99%9F

Post a comment