早先已展示過,在網頁內嵌Google地圖將地址轉換為經緯度座標在地圖上顯示自訂地標圖示等技巧,最後來個綜合應用當作期末考。本次的練習題是"依使用者所在位置,找出距離最近的五個台北市消防分隊"。

簡單整理值得留意的技術細節:

  1. 在網頁嵌入Google地圖並放上自訂標示點(Marker)的做法,可參考筆記-網頁內嵌Google地圖與地理位置模擬一文。
  2. 由市政府網站取得台北市消防分隊地址,透過地理編碼算出經緯度座標,可參考Google Maps API地址轉換一文。而在本次範例中,我們預先將查到的經緯度數字一併寫入CSV檔中,不必每次重新查詢。
  3. 要計算兩個經緯度座標間的直線距離,Haversine公式是最常用的演算法,簡單來說,就是把地球當成一個圓球,用球體表面任兩點到圓心所形成的夾角,加上一堆Sin, Cos推算沿球體表面連接兩點的弧線長度。依據英國學者研究指出,思考過度複雜數學公式可能會對中老年人的神經中樞有負面影響,為求養生保健,在此直接引用公式,對於數學細節就不再深究...
  4. HTML5世代的瀏覽器(IE要IE9+)多能支援地理資訊功能,可整合行動裝置(手機、平版)的GPS取得使用者當時所在地理位置(存取前會彈出確認視窗徵求使用者同意),如此我們便可依使用者所在位置提供不同資訊,例如: 列出臨近的商店、餐廳或服務據點... 等等。要透過Javascript存取使用者的地理資訊,可使用Geolocation API
  5. 使用者所在位置及各消防分隊的經緯度都有了,便可利用Haversine公式算出各分隊與目前位置的直線距離作為遠近參考。(不考慮路線規劃、交通狀況等因素,那是導航軟體或霹靂車RD該煩惱的事,為了好玩寫程式沒必要把自己逼上絕路 XD 至於有心挑戰的朋友,Google Maps也有路線規劃API,倒是可以參考)
  6. Javascript的Array.sort(compareFunction),提供了類似LINQ OrderBy(o => o.prop)的簡便做法,讓我驚喜了一下。原本以為要花點心思處理,沒想到只用兩行就搞定依距離遠近排序的需求。
  7. 前篇文章介紹的動態文字圖檔權充Marker Icon派上用場,直接用分隊名稱告示牌當標示圖,最近的前五名用鮮紅底,其餘用暗紅底,一目了然,酷!!

程式範例如下,請享用:

<!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>
    <script src="DynaMarkerIcon.js" type="text/javascript"></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 () {
            //計算兩個經緯座標間的距離(Haversine公式)
            function distHaversine(p1, p2) {
                var rad = function (x) { return x * Math.PI / 180; }
                var R = 6371; // earth's mean radius in km
                var dLat = rad(p2.lat() - p1.lat());
                var dLong = rad(p2.lng() - p1.lng());
 
                var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                        Math.cos(rad(p1.lat())) * Math.cos(rad(p2.lat()))
                        * Math.sin(dLong / 2) * Math.sin(dLong / 2);
                var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                var d = R * c;
 
                return parseFloat(d.toFixed(3));
            }
            //消防分局資料陣列
            var branches = [];
            //取得分局資料(含經緯座標),存為物作陣列
            $.get("AddrList.txt", {}, function (list) {
                var lines = list.replace(/\r/g, "").split('\n');
                //lines[i]格式如下:
                //文山中隊-木柵分隊,(02)29391604,文山區木柵路2段200號,24.9890353,121.5630845 
                for (var i = 0; i < lines.length; i++) {
                    var parts = lines[i].split(',');
                    branches.push({
                        name: parts[0],
                        tel: parts[1],
                        addr: parts[2],
                        latlng: new google.maps.LatLng(
                                    parseFloat(parts[3]), parseFloat(parts[4])),
                        dist: 0
                    });
                }
                getGeolocation();
            });
            //取得使用者目前位罝
            function getGeolocation() {
                if (navigator && navigator.geolocation) {
                    //getCurrentPosition屬非同步執行,要另定函數解析結果
                    navigator.geolocation.getCurrentPosition(parsePosition);
                }
            }
            //解析getCurrentPosition傳回的結果
            function parsePosition(pos) {
                //標示點陣列
                var markers = [];
                //由pos.coords取出latitude及longitude
                var curLatLng = new google.maps.LatLng(
                        pos.coords.latitude, pos.coords.longitude);
                //分別計算對所有Branch的距離
                for (var i = 0; i < branches.length; i++) {
                    var branch = branches[i];
                    branch.distance = //計算兩個LatLng間的距離
                            distHaversine(branch.latlng, curLatLng);
                }
                //依距離進行排序
                branches.sort(function (a, b) {
                    if (a.distance == b.distance) return 0;
                    return (a.distance > b.distance) ? 1 : -1;
                });
 
                //設定地圖參數
                var mapOptions = {
                    center: curLatLng,
                    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();
                //加入使用者所在位置
                var marker = new google.maps.Marker({
                    position: curLatLng,
                    title: "現在位置",
                    //借用前篇文章介紹的Canvas.toDataURL()產生動態圖檔作為圖示
                    icon: createMarkerIcon("現在位置"),
                    map: map
                });
                var h = [];
                //因為已排序過,故會依距離由近到遠加入Marker
                for (var i = 0; i < branches.length; i++) {
                    var b = branches[i];
                    //距離最近的前五名加入檢視範圍
                    if (i < 5) bounds.extend(b.latlng);
                    var marker = new google.maps.Marker({
                        position: b.latlng,
                        title: b.name, //以刻有分隊名稱的告示牌作為圖示
                        icon: createMarkerIcon(b.name.split('-')[1],
                                //距離較近的前五名為紅底,其餘為暗紅底
                                { bgColor: i < 5 ? 'red' : 'darkred' }),
                        map: map
                    });
                }
                //調整檢視範圍
                map.fitBounds(bounds);
            }
        });
    </script>
</head>
<body>
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>

執行結果如下:


Comments

# by 涂聚文

DynaMarkerIcon.js 請問能不能公開這個文個的代碼?謝謝。 geovindu@163.com.

# by Jeffrey

to 涂聚文, DynaMarkerIcon.js只有一個createMarkerIcon()函數,程式碼可在另一篇文章(http://blog.darkthread.net/post-2012-06-16-canvas-dynamic-image.aspx)找到。

# by bee

您好請教一下branch.distance = distHaversine(branch.latlng, curLatLng);這行是會自動在branches陣列中新增一個名稱叫distance的欄位嗎?因為這樣在下面距離排序的sort function中才可以用distance這個欄位去做排序的動作對嗎?感謝您~

# by Jeffrey

to bee, 是的,該段運算會計算branches陣列中的每個據點branch物件的座標,與GPS取得當下座標間的直線距離,並存為branch的distance屬性,作為後續排序的基準。

# by XINKAI

您好,最近在做類似的專題 想從您的程式碼下去摸索 可是我將程式碼複製之後 貼進dreamweaver裡面 然後存成html, 但是開啟後一片空白沒有東西 不知道是不是我哪裡做錯了呢? 感謝您費時回答=]

# by Jeffrey

to XINKAI, 本文所附程式碼僅為範例示意,還需要自行由先前文章擷取DynaMarkerIcon.js及AddrList.txt搭配應用(我已另外做了一個單一HTML檔就可執行的範例: https://www.facebook.com/groups/darkthread/595227677227917/)。 另一方面,瀏覽器必須支援地理位置偵測(依我實測,放在本機檔案直接使用Chrome開啟,地理偵測預設會被停用,故放在Web Server測試較簡便)。

# by XINKAI

非常謝謝您的回答!!!!! 我有到您的社團中下載檔案, 下載之後開啟如您所說的 以Chrome開啟沒有任何反應... 我嘗試以Firefox開啟,視窗有詢問地理資訊 但是我選擇"透露地理位置"後依舊沒有地圖顯示 後來我放入學校提供的ftp空間 以Chrome開啟, 這次有詢問是否透露地理位置 但也是沒有顯示地圖 Firefox亦是如此... 不知道這是不是我電腦的關係呢? 感謝您費時回答=]

# by Jeffrey

to XINKAI, 我猜是你的電腦環境不支援地理偵測功能導致,我改了一個不需偵測地理位置的版本(http://jsbin.com/tazem/2),如果這個網頁OK,應可推論問題出在GPS偵測。

# by XINKAI

看來似乎是我自己電腦的問題!!!!! 我到學校的電腦用就可以了=目 所以上面那篇就沒問題了~~~ 感謝!!!! 另外我想詢問"現在位置"的定位 是不是會有些差距呢?? http://www.cs.pu.edu.tw/~s1002439/GoogleMapSample.png 我人在沙鹿區(紅圈處) 但是"現在位置"是在台中的中區 請問這是正常的差距範圍內嗎? 還是有辦法可以定位較正確的位置 非常感謝您的回答!!!!!!

# by Jeffrey

to XINKAI, 瀏覽器會透過一些方式試著定位你的經緯度,例如: GPS接收器、無線分享器、IP位址/ISP資訊... 等等。行動裝置內建GPS接收衛星訊息較精準,其餘方式都屬推估性質,常與實際位置有誤差。我自己的經驗,若走有線網路上網又在防火牆內的PC,時常定位到ISP機房。一般而言,地理定位應用在行動裝置較可靠,不太適合有線網路的情境。

# by XINKAI

真的非常謝謝您的回答=] 了解很多!!!! 下次有遇到問題再向您請益噢~~ 感恩=]

# by 嘿嘿宅男

請問function distHaversine() 計算出來的距離單位是公尺還是公里?

# by Jeffrey

to 嘿嘿宅男, 帶入的R = 6371; // earth's mean radius in km ,故計算結果應為公里

# by XINKAI

版大您好~~ 我又來了:P 這次我想請問程式碼中有寫入指定的地點和電話 請問要怎麼使他顯示出來? 我很天真的以為左鍵一個click就有了... 還是寫入這些資訊需要其他的程式碼 才有辦法讓他點擊後跳出小視窗?! 麻煩您費時回答了感謝=]

# by Jeffrey

to XINKAI, 我對Google地圖的互動式整合應用就沒研究了,這部分抱歉幫不上忙。

# by XINKAI

版大您好, 本來以為程式碼中有電話跟地址這部分 可以直接顯示,原來無法... 那我想要把電話和地址拿掉 我曾經嘗試把電話與地址拿掉變成以下這樣 忠孝分隊,25.043710,121.527050 然後把座標改為 parseFloat(parts[1]), parseFloat(parts[2])) 但沒有辦法顯示 是我還有哪邊沒有改到嗎 還是不是我這樣改就可以的~~ 需要再另外設計? 下面是您抓取lines[i]資料的程式碼 for (var i = 0; i < lines.length; i++) { var parts = lines[i].split(','); branches.push({ name: parts[0], tel: parts[1], addr: parts[2], latlng: new google.maps.LatLng( parseFloat(parts[3]), parseFloat(parts[4])), dist: 0 }); } 再次麻煩您了,感謝=]

# by Jeffrey

to XINKAI, 去除地址電話的版本: http://jsbin.com/tazem/3/edit

# by XINKAI

板大~~ 我看程式碼裡面多了 //停用地理偵測,改為固定位置 parsePosition({ coords: { latitude: 25.0299894, longitude: 121.5630845 } }); 為什麼把上面那些隱藏起來就沒辦法偵測?? 您提供的那個網站好好用!!!!! 可以馬上顯示~~~ 板大,不好意思噢,麻煩您費時回答了=]

# by Jeffrey

to XINKAI, 地理位置自動偵測需要瀏覽器及作業環境支援,http://jsbin.com/tazem/4/edit <== 自動偵測版。關鍵在於getGeolocation();

# by 成宏

板大你好...請問如果用我的位置和距離較近分隊做出路徑規劃要如何改? 謝謝您回應

# by Jeffrey

to 成宏,路徑規劃有另外的API,我沒有實際應用經驗,可參考: http://www.dotblogs.com.tw/atowngit/archive/2011/01/11/20746.aspx。

# by Joseph Qi

你好 我想請問: getCurrentPosition 是目前位置 watchPosition 會一直更新目前位置 可是 如果我沒有靠需要定位的app放在背景運作的話 他沒辦法自己呼叫gps做實時更新 這方面有解答嗎? 感謝

# by Jeffrey

to Joseph Qi, 不確定有沒有誤解你的意思,但如果只是想不斷更新目前的位置,用setInterval(function() { ...重新取得位置邏輯... }, 30 * 1000);應該就能30秒更新一次。

# by PAUL

安 我剛剛看了你寫的程式 我會轉到手機 請問是執行出來直接可以定位(現在的位置)嗎(不管走道哪裡都會變)? 因為我跑出來是一張地圖而已! 拜託幫個忙!!!謝謝

# by Jeffrey

to PAUL, 在支援GPS定位的裝置瀏覽http://jsbin.com/tazem/4 ,會看到地圖、消防分隊標籤及一個藍色標籤【現在位置】,該標籤所在即為程式偵測到的地理位置。

# by PAUL

如果我是要不斷更新目前的位置 setInterval(function() { ...重新取得位置邏輯... }, 30 * 1000); 這段我應該放在程式裡的哪裡嗎? 謝謝你 我是新手 可以講詳細一點沒關西

# by Jeffrey

to PAUL, 直覺想法是setInterval定期呼叫navigator.geolocation.getCurrentPosition(座標解析函式)取得最新座標,再移動地圖上的Marker到新位置,感覺不難但我沒實做過,之後有時間或許會試試。

# by PAUL

var int=self.setInterval("()"navigator.geolocation.getCurrentPosition,50) 我試了幾天 是這樣寫嗎?可是好像還是不行 謝謝你

# by Jeffrey

to PAUL, 是window.setInterval()而非self.setInterval(), 而setInterval("()" navigator.geolocation.getCurrentPosition也不是有效的JavaScript語法。 不知道你過去有沒有寫JavaScript的經驗?如果從未接觸過,從這個應用入門有點辛苦(依我自己的學習經驗),而後續修改應用需要一些JavaScript基礎才好上手,先花點時間練基本功(反正,JS目前是很值得投資的語言)會比較順利。推薦:http://openhome.cc/Gossip/JavaScript/

# by PAUL

恩恩 我沒有碰過 我不是讀本科的 我是想加在我的畢業專題裡 但我會在試試看 我想問你這個定位的方試是用電腦IP定位還是電腦GPS(這個應該不一樣吧) 因為我主要想把他做成APP 也就是說開手機的GPS定位就可以了 還是是因為電腦IP的定位 這樣我手機就沒辦法用了嗎?(因為我前幾天把他包成APP看 好像跑不起來) 謝謝你回答我問題! PS:我的APP是我自幾報告用而已 沒有要賺錢!

# by PAUL

你以上的語法不是用HTML5寫的嘛?為什麼之後須要用JavaScript呢? 如果這樣改接下面是可行的嗎?

# by Jeffrey

to PAUL, 網頁的很多功能需要JavaScript才能實現,當我們說HTML5時,它的範圍其實包含HTML標籤、CSS、JavaScript。在沒有GPS接收器的裝置上,可透過IP定位找出所在位置,但精準度較差,也可能會被誤導到網路機房所在位置。我們在寫網頁程式時不用擔心(也無法控制)要用IP或是GPS定位,瀏覽器會依裝置的支援能力決定。

# by PAUL

謝謝你耶!我在研究看看

# by PAUL

我有個問題 就是如果我想要在APP裡面 加一個按鈕 (算是2層)重自身的APP理按鈕 按下後可以跑出另一個APP的城市與介面 這是可行的嗎? 需要寫哪些程式語法 謝謝

# by Jeffrey

to PAUL, 不同APP開發工具不同裝置的做法都不相同,以PhoneGap @Android 為例,可考慮WebIntent(http://smus.com/android-phonegap-plugins/),但APP開發已非我專長領域,恕無法再提供更多細節~

# by UF

請問HTML5有辦法做到偵測客戶端IP並且顯示嗎?

# by Jeffrey

to UF, 依我所知,客戶端IP無法直接由瀏覽器取得,需透過Server端程式協助,網路上有現成API服務(甚至由IP查出國家地區)但多半要收費,但查詢IP API自己要寫也很容易,一兩行搞定。

# by elephant

我想要利用我的位置的經緯度跟我資料庫裡面的店家(資料庫中有每個店家的經緯度)去計算距離 然後在另外一個頁面用listview方式顯示他們的資訊 但是我不知道怎麼抓取我現在位置的經緯度... 麻煩惹 :(

# by Jeffrey

to elephant, 可參考navigator.geolocation.getCurrentPosition(parsePosition)寫法,在function parsePosition(pos)可由pos.coords.latitude, pos.coords.longitude取得經緯度。

# by elephant

不好意思 想在請教一下 這個程式顯示出來的標籤 可以改成可以點擊的嗎 我想要直接可以點擊然後跳到該店家的網站 麻煩你了~~

# by Jeffrey

to elephant, Marker支援click事件,應可加上連至店家網站的程式。參考:https://developers.google.com/maps/documentation/javascript/examples/event-simple?hl=zh-tw

# by elephant

我有研究了一下... 應該是要加入這段程式碼 google.maps.event.addListener(marker, 'click', function() { map.setZoom(8); map.setCenter(marker.getPosition()); }); } 但是不管我加在哪....整個畫面都會變空白....怎麼這樣 :((

# by Jeffrey

to elephant, 我做了一個範例,點下Marker會開啟本部落格:http://jsbin.com/tazem/5

# by elephant

不好意思 又來麻煩你了... 就是我想問一下 這個程式可以篩選距離嗎 例如超過現在位置五公里以外就不顯示 可以嗎~~

# by Jeffrey

to elephant, 程式中的distHaversine函式可計算兩座標間的直線距離,亦即以某點為圓心,偵測特定半徑內的其他座標。

# by Sam

版主您好 最近在製作類似的專題,看了您的文章之後覺得有很大幫助 小弟想再跟您請教 若我想要在地圖上自定義一個區塊 然後判斷GPS傳回的定位點是否有落在這個區塊內 這部分要朝哪個方向去找相關資源呢? Google Map是否有提供類似的API? 感謝您的分享 同時也謝謝您的回覆

# by Jeffrey

to Sam, 聽起來像是幾何數學。以矩形區域為例,由四個角的經緯度座標可得X1,X2,Y1,Y2,GPS傳回定位點X,Y,比對是否X介於X1與X2間,Y介於Y1與Y2間,就能判斷是否落於區域內。若是圓形則可用與圓心距離有沒有超過半徑決定,區域形狀愈複雜,判斷的演算法也愈複雜,但用數學應該都能求解。

# by Sam

板主您好 我有找到相關資源了 Google Maps API 有提供一個containsLocation()可以直接判斷 https://developers.google.com/maps/documentation/javascript/examples/poly-containsLocation

# by Jeffrey

to Sam, WOW!有現成的Polygon範圍與座標比對功能,不用自己搞幾何計算實在是太貼心了。謝謝你的回饋分享!

# by Milton Chu

謝謝分享, 參考您的範例, 我將會運用在實際的專案上. 在開發過程式, 發現Chrome會檢查是否走https, 如果不是, 就取不到位子. 而且沒有提示是否要取用目前位子. 在IE上, 不論http / https 都出現是否允許存取目前位子.

# by Jeffrey

to Milton Chu, 謝謝你的補充。

# by marco

版主您好,很慶幸能在茫茫網頁海中遇到黑暗執行續這盞明燈, 想請問如果使用navigator.geolocation.getCurrentPosition找到使用者目前座標後要怎麼儲存至資料庫? 查了一下資料,似乎要連結sql資料庫好像有一定的風險,因為帳號密碼都大辣辣地顯示給大家看了,想請教有沒有比較好的方法?

# by Jeffrey

to marco, 寫入資料庫的邏輯通常會寫在伺服器端的程式執行(例如:PHP、ASP.NET、Java、Node.js、Ruby),提供一個API接受前端傳入座標寫入資料庫,不會有帳號密碼直接曝露的問題,但要伺服器端程式又是另一門學問。

Post a comment