利用行動裝置GPS定位尋找臨近地點

早先已展示過,在網頁內嵌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>

執行結果如下:

歡迎推文分享:
Published 19 June 2012 06:45 AM 由 Jeffrey
Filed under:
Views: 62,589



意見

# 涂聚文 said on 07 September, 2012 08:05 AM

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

# Jeffrey said on 07 September, 2012 06:47 PM

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

# bee said on 21 December, 2012 03:39 AM

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

# Jeffrey said on 21 December, 2012 07:21 PM

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

# XINKAI said on 20 February, 2014 03:53 AM

您好,最近在做類似的專題

想從您的程式碼下去摸索

可是我將程式碼複製之後

貼進dreamweaver裡面

然後存成html,

但是開啟後一片空白沒有東西

不知道是不是我哪裡做錯了呢?

感謝您費時回答=]

# Jeffrey said on 20 February, 2014 08:15 AM

to XINKAI, 本文所附程式碼僅為範例示意,還需要自行由先前文章擷取DynaMarkerIcon.js及AddrList.txt搭配應用(我已另外做了一個單一HTML檔就可執行的範例: www.facebook.com/.../595227677227917)。

另一方面,瀏覽器必須支援地理位置偵測(依我實測,放在本機檔案直接使用Chrome開啟,地理偵測預設會被停用,故放在Web Server測試較簡便)。

# XINKAI said on 25 February, 2014 09:49 AM

非常謝謝您的回答!!!!!

我有到您的社團中下載檔案,

下載之後開啟如您所說的

以Chrome開啟沒有任何反應...

我嘗試以Firefox開啟,視窗有詢問地理資訊

但是我選擇"透露地理位置"後依舊沒有地圖顯示

後來我放入學校提供的ftp空間

以Chrome開啟,

這次有詢問是否透露地理位置

但也是沒有顯示地圖

Firefox亦是如此...

不知道這是不是我電腦的關係呢?

感謝您費時回答=]

# Jeffrey said on 26 February, 2014 01:08 AM

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

# XINKAI said on 26 February, 2014 01:23 AM

看來似乎是我自己電腦的問題!!!!!

我到學校的電腦用就可以了=目

所以上面那篇就沒問題了~~~

感謝!!!!

另外我想詢問"現在位置"的定位

是不是會有些差距呢??

www.cs.pu.edu.tw/.../GoogleMapSample.png

我人在沙鹿區(紅圈處)

但是"現在位置"是在台中的中區

請問這是正常的差距範圍內嗎?

還是有辦法可以定位較正確的位置

非常感謝您的回答!!!!!!

# Jeffrey said on 26 February, 2014 08:02 AM

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

# XINKAI said on 27 February, 2014 12:21 AM

真的非常謝謝您的回答=]

了解很多!!!!

下次有遇到問題再向您請益噢~~

感恩=]

# 嘿嘿宅男 said on 01 March, 2014 10:04 AM

請問function distHaversine()

計算出來的距離單位是公尺還是公里?

# Jeffrey said on 02 March, 2014 05:51 AM

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

# XINKAI said on 05 March, 2014 02:54 AM

版大您好~~

我又來了:P

這次我想請問程式碼中有寫入指定的地點和電話

請問要怎麼使他顯示出來?

我很天真的以為左鍵一個click就有了...

還是寫入這些資訊需要其他的程式碼

才有辦法讓他點擊後跳出小視窗?!

麻煩您費時回答了感謝=]

# Jeffrey said on 05 March, 2014 04:55 AM

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

# XINKAI said on 12 March, 2014 02:38 AM

版大您好,

本來以為程式碼中有電話跟地址這部分

可以直接顯示,原來無法...

那我想要把電話和地址拿掉

我曾經嘗試把電話與地址拿掉變成以下這樣

忠孝分隊,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

});

}

再次麻煩您了,感謝=]

# Jeffrey said on 12 March, 2014 11:17 AM

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

# XINKAI said on 13 March, 2014 03:07 AM

板大~~

我看程式碼裡面多了

//停用地理偵測,改為固定位置

         parsePosition({ coords: { latitude: 25.0299894, longitude: 121.5630845 } });

為什麼把上面那些隱藏起來就沒辦法偵測??

您提供的那個網站好好用!!!!!

可以馬上顯示~~~

板大,不好意思噢,麻煩您費時回答了=]

# Jeffrey said on 13 March, 2014 09:41 AM

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

# 成宏 said on 09 April, 2014 07:30 AM

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

# Jeffrey said on 10 April, 2014 12:14 AM

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

# Joseph Qi said on 18 April, 2014 01:05 PM

你好 我想請問:

getCurrentPosition 是目前位置

watchPosition 會一直更新目前位置

可是 如果我沒有靠需要定位的app放在背景運作的話

他沒辦法自己呼叫gps做實時更新

這方面有解答嗎?  感謝

# Jeffrey said on 19 April, 2014 12:04 AM

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

# PAUL said on 10 November, 2014 04:52 AM

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

拜託幫個忙!!!謝謝

# Jeffrey said on 10 November, 2014 05:10 AM

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

# PAUL said on 10 November, 2014 05:48 AM

如果我是要不斷更新目前的位置

setInterval(function() { ...重新取得位置邏輯... }, 30 *  1000);

這段我應該放在程式裡的哪裡嗎?

謝謝你 我是新手 可以講詳細一點沒關西

# Jeffrey said on 11 November, 2014 08:19 AM

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

# PAUL said on 20 November, 2014 04:01 AM

var int=self.setInterval("()"navigator.geolocation.getCurrentPosition,50)

我試了幾天

是這樣寫嗎?可是好像還是不行

謝謝你

# Jeffrey said on 20 November, 2014 07:23 PM

to PAUL, 是window.setInterval()而非self.setInterval(), 而setInterval("()" navigator.geolocation.getCurrentPosition也不是有效的JavaScript語法。

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

# PAUL said on 29 November, 2014 10:25 AM

恩恩 我沒有碰過 我不是讀本科的 我是想加在我的畢業專題裡 但我會在試試看

我想問你這個定位的方試是用電腦IP定位還是電腦GPS(這個應該不一樣吧) 因為我主要想把他做成APP 也就是說開手機的GPS定位就可以了 還是是因為電腦IP的定位 這樣我手機就沒辦法用了嗎?(因為我前幾天把他包成APP看 好像跑不起來)

謝謝你回答我問題!

PS:我的APP是我自幾報告用而已 沒有要賺錢!

# PAUL said on 29 November, 2014 10:29 AM

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

# Jeffrey said on 30 November, 2014 08:27 PM

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

# PAUL said on 07 December, 2014 11:40 AM

謝謝你耶!我在研究看看

# PAUL said on 16 December, 2014 11:24 PM

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

謝謝

# Jeffrey said on 17 December, 2014 07:29 AM

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

# UF said on 14 March, 2015 10:40 AM

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

# Jeffrey said on 14 March, 2015 10:51 AM

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

# elephant said on 19 March, 2015 02:50 AM

我想要利用我的位置的經緯度跟我資料庫裡面的店家(資料庫中有每個店家的經緯度)去計算距離  然後在另外一個頁面用listview方式顯示他們的資訊

但是我不知道怎麼抓取我現在位置的經緯度...

麻煩惹 :(

# Jeffrey said on 19 March, 2015 09:21 PM

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

# elephant said on 13 April, 2015 12:55 PM

不好意思 想在請教一下

這個程式顯示出來的標籤 可以改成可以點擊的嗎

我想要直接可以點擊然後跳到該店家的網站

麻煩你了~~

# Jeffrey said on 14 April, 2015 01:17 AM

to elephant, Marker支援click事件,應可加上連至店家網站的程式。參考:developers.google.com/.../event-simple

# elephant said on 14 April, 2015 03:49 AM

我有研究了一下...

應該是要加入這段程式碼

 google.maps.event.addListener(marker, 'click', function() {

   map.setZoom(8);

   map.setCenter(marker.getPosition());

 });

}

但是不管我加在哪....整個畫面都會變空白....怎麼這樣 :((

# Jeffrey said on 14 April, 2015 06:10 AM

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

# elephant said on 14 May, 2015 02:04 PM

不好意思 又來麻煩你了...

就是我想問一下

這個程式可以篩選距離嗎

例如超過現在位置五公里以外就不顯示  可以嗎~~

# Jeffrey said on 14 May, 2015 06:12 PM

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

# Sam said on 18 November, 2015 11:49 AM

版主您好

最近在製作類似的專題,看了您的文章之後覺得有很大幫助

小弟想再跟您請教

若我想要在地圖上自定義一個區塊

然後判斷GPS傳回的定位點是否有落在這個區塊內

這部分要朝哪個方向去找相關資源呢?

Google Map是否有提供類似的API?

感謝您的分享 同時也謝謝您的回覆

# Jeffrey said on 18 November, 2015 05:51 PM

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

# Sam said on 19 November, 2015 02:40 AM

板主您好

我有找到相關資源了

Google Maps API 有提供一個containsLocation()可以直接判斷

developers.google.com/.../poly-containsLocation

# Jeffrey said on 19 November, 2015 04:48 AM

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

# Milton Chu said on 06 August, 2016 01:35 AM

謝謝分享, 參考您的範例, 我將會運用在實際的專案上.

在開發過程式, 發現Chrome會檢查是否走https, 如果不是, 就取不到位子. 而且沒有提示是否要取用目前位子.

在IE上, 不論http / https 都出現是否允許存取目前位子.

# Jeffrey said on 06 August, 2016 04:26 AM

to Milton Chu, 謝謝你的補充。

# marco said on 03 February, 2017 03:54 AM

版主您好,很慶幸能在茫茫網頁海中遇到黑暗執行續這盞明燈,

想請問如果使用navigator.geolocation.getCurrentPosition找到使用者目前座標後要怎麼儲存至資料庫?

查了一下資料,似乎要連結sql資料庫好像有一定的風險,因為帳號密碼都大辣辣地顯示給大家看了,想請教有沒有比較好的方法?

# Jeffrey said on 03 February, 2017 07:55 AM

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

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<June 2012>
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication