利用行动装置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%;%20height:100%"></div>
</body>
</html>

执行结果如下:

原文:http://blog.darkthread.net/post-2012-06-19-find-nearest-geolocation-markers.aspx