如何在Google地图折线上根据距离添加标记?

17

我正在尝试创建一个谷歌地图,用户可以在上面标出他走过/跑过/骑过的路线,并查看他跑了多长时间。为此,GPolyline类及其getLength() 方法非常有帮助(至少对于Google Maps API V2),但我想添加基于距离的标记,例如1公里,5公里,10公里等,但似乎没有明显的方法可以根据沿着线路的距离找到一点。 有什么建议吗?

5个回答

36

我在几个月前回答了一个类似的问题,是如何在SQL Server 2008服务器端处理此问题。现在我将相同的算法移植到JavaScript中,使用Google Maps API v2

为了举例说明,我们使用一个简单的4点折线,总长度约为8800米。下面的片段将定义这条折线并在地图上呈现:

var map = new GMap2(document.getElementById('map_canvas'));

var points = [
   new GLatLng(47.656, -122.360),
   new GLatLng(47.656, -122.343),
   new GLatLng(47.690, -122.310),
   new GLatLng(47.690, -122.270)
];

var polyline = new GPolyline(points, '#f00', 6);

map.setCenter(new GLatLng(47.676, -122.343), 12);
map.addOverlay(polyline);

在我们开始实际算法之前,我们需要一个函数,当给定起始点、终止点和沿该线路行驶的距离时,返回目标点。幸运的是,Chris Veness在计算纬度/经度点之间距离、方位和更多内容中提供了一些方便的JavaScript实现。

特别地,我已经从上述来源中调整了以下两种方法以与Google的GLatLng类一起使用:

这些方法被用来扩展Google的GLatLng类,并创建了一个moveTowards()方法,当给定另一个点和一个距离(以米为单位)时,它将返回从原点沿那条线路向传递的点前进指定距离后的另一个GLatLng

GLatLng.prototype.moveTowards = function(point, distance) {   
   var lat1 = this.lat().toRad();
   var lon1 = this.lng().toRad();
   var lat2 = point.lat().toRad();
   var lon2 = point.lng().toRad();         
   var dLon = (point.lng() - this.lng()).toRad();

   // Find the bearing from this point to the next.
   var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
                         Math.cos(lat1) * Math.sin(lat2) -
                         Math.sin(lat1) * Math.cos(lat2) * 
                         Math.cos(dLon));

   var angDist = distance / 6371000;  // Earth's radius.

   // Calculate the destination point, given the source and bearing.
   lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) + 
                    Math.cos(lat1) * Math.sin(angDist) * 
                    Math.cos(brng));

   lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
                            Math.cos(lat1), 
                            Math.cos(angDist) - Math.sin(lat1) *
                            Math.sin(lat2));

   if (isNaN(lat2) || isNaN(lon2)) return null;

   return new GLatLng(lat2.toDeg(), lon2.toDeg());
}

有了这个方法,我们现在可以按照以下方式解决问题:

  1. 遍历路径中的每个点。
  2. 找到迭代中当前点到下一个点之间的距离。
  3. 如果第2步中的距离大于我们需要沿着路径行进的距离:

    ...那么目标点就在这个点和下一个点之间。只需向当前点应用 moveTowards() 方法,传递下一个点和需要行进的距离。返回结果并结束迭代。

    否则:

    ...目标点在迭代中下一个点的后面更远处。我们需要从沿着路径行进的总距离中减去此点与下一个点之间的距离。使用修改后的距离继续迭代。

您可能已经注意到,我们可以很容易地通过递归来实现上述过程,而不是通过迭代。因此让我们这样做:

function moveAlongPath(points, distance, index) {
   index = index || 0;  // Set index to 0 by default.

   if (index < points.length) {
      // There is still at least one point further from this point.

      // Construct a GPolyline to use its getLength() method.
      var polyline = new GPolyline([points[index], points[index + 1]]);

      // Get the distance from this point to the next point in the polyline.
      var distanceToNextPoint = polyline.getLength();

      if (distance <= distanceToNextPoint) {
         // distanceToNextPoint is within this point and the next. 
         // Return the destination point with moveTowards().
         return points[index].moveTowards(points[index + 1], distance);
      }
      else {
         // The destination is further from the next point. Subtract
         // distanceToNextPoint from distance and continue recursively.
         return moveAlongPath(points,
                              distance - distanceToNextPoint,
                              index + 1);
      }
   }
   else {
      // There are no further points. The distance exceeds the length  
      // of the full path. Return null.
      return null;
   }  
}

使用上述方法,如果我们定义了一个GLatLng点的数组,并且我们使用这个点数组以及距离为2,500米来调用我们的moveAlongPath()函数,它将返回该路径上距离第一个点2.5公里处的GLatLng

var points = [
   new GLatLng(47.656, -122.360),
   new GLatLng(47.656, -122.343),
   new GLatLng(47.690, -122.310),
   new GLatLng(47.690, -122.270)
];

var destinationPointOnPath = moveAlongPath(points, 2500);

// destinationPointOnPath will be a GLatLng on the path 
// at 2.5km from the start.
因此,我们只需要为路径上的每个检查点调用 moveAlongPath()。如果你需要在1km、5km和10km处有三个标记点,你可以简单地这样做:
因此,我们只需为路径上每个检查点调用 moveAlongPath()。如果您需要在1公里、5公里和10公里处有三个标记,则可以简单执行以下操作:
map.addOverlay(new GMarker(moveAlongPath(points, 1000)));
map.addOverlay(new GMarker(moveAlongPath(points, 5000)));
map.addOverlay(new GMarker(moveAlongPath(points, 10000)));

请注意,moveAlongPath()可能会返回null,如果我们请求的检查点距路径总长度更远,因此在传递给new GMarker()之前最好先检查返回值。

我们可以将这些内容组合起来以完成完整的实现。在此示例中,我们沿着之前定义的8.8公里路径每隔1,000米放置一个标记:

<!DOCTYPE html>
<html> 
<head> 
   <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 
   <title>Google Maps - Moving point along a path</title> 
   <script src="http://maps.google.com/maps?file=api&v=2&sensor=false"
           type="text/javascript"></script> 
</head> 
<body onunload="GUnload()"> 
   <div id="map_canvas" style="width: 500px; height: 300px;"></div>

   <script type="text/javascript"> 

   Number.prototype.toRad = function() {
      return this * Math.PI / 180;
   }

   Number.prototype.toDeg = function() {
      return this * 180 / Math.PI;
   }

   GLatLng.prototype.moveTowards = function(point, distance) {   
      var lat1 = this.lat().toRad();
      var lon1 = this.lng().toRad();
      var lat2 = point.lat().toRad();
      var lon2 = point.lng().toRad();         
      var dLon = (point.lng() - this.lng()).toRad();

      // Find the bearing from this point to the next.
      var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
                            Math.cos(lat1) * Math.sin(lat2) -
                            Math.sin(lat1) * Math.cos(lat2) * 
                            Math.cos(dLon));

      var angDist = distance / 6371000;  // Earth's radius.

      // Calculate the destination point, given the source and bearing.
      lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) + 
                       Math.cos(lat1) * Math.sin(angDist) * 
                       Math.cos(brng));

      lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
                               Math.cos(lat1), 
                               Math.cos(angDist) - Math.sin(lat1) *
                               Math.sin(lat2));

      if (isNaN(lat2) || isNaN(lon2)) return null;

      return new GLatLng(lat2.toDeg(), lon2.toDeg());
   }

   function moveAlongPath(points, distance, index) {        
      index = index || 0;  // Set index to 0 by default.

      if (index < points.length) {
         // There is still at least one point further from this point.

         // Construct a GPolyline to use the getLength() method.
         var polyline = new GPolyline([points[index], points[index + 1]]);

         // Get the distance from this point to the next point in the polyline.
         var distanceToNextPoint = polyline.getLength();

         if (distance <= distanceToNextPoint) {
            // distanceToNextPoint is within this point and the next. 
            // Return the destination point with moveTowards().
            return points[index].moveTowards(points[index + 1], distance);
         }
         else {
            // The destination is further from the next point. Subtract
            // distanceToNextPoint from distance and continue recursively.
            return moveAlongPath(points,
                                 distance - distanceToNextPoint,
                                 index + 1);
         }
      }
      else {
         // There are no further points. The distance exceeds the length  
         // of the full path. Return null.
         return null;
      }  
   }

   var map = new GMap2(document.getElementById('map_canvas'));

   var points = [
      new GLatLng(47.656, -122.360),
      new GLatLng(47.656, -122.343),
      new GLatLng(47.690, -122.310),
      new GLatLng(47.690, -122.270)
   ];

   var polyline = new GPolyline(points, '#f00', 6);

   var nextMarkerAt = 0;     // Counter for the marker checkpoints.
   var nextPoint = null;     // The point where to place the next marker.

   map.setCenter(new GLatLng(47.676, -122.343), 12);

   // Draw the path on the map.
   map.addOverlay(polyline);

   // Draw the checkpoint markers every 1000 meters.
   while (true) {
      // Call moveAlongPath which will return the GLatLng with the next
      // marker on the path.
      nextPoint = moveAlongPath(points, nextMarkerAt);

      if (nextPoint) {
         // Draw the marker on the map.
         map.addOverlay(new GMarker(nextPoint));

         // Add +1000 meters for the next checkpoint.
         nextMarkerAt += 1000;    
      }
      else {
         // moveAlongPath returned null, so there are no more check points.
         break;
      }            
   }
   </script>
</body> 
</html>

上面示例的截图,显示每1000米的标记:

Google Maps - Move Point Along a Path


我正在使用Google Map Api V3,您的公式似乎很好,但是当我缩放到道路级别时,我可以看到谷歌绘制的线和我的标记之间有一定距离。这是为什么呢? - Nordes
@Nordes:以上示例是否出现此问题?我尝试将缩放级别最大化,标记似乎在线上。截图:http://img408.imageshack.us/img408/8687/gmapnospace.png - Daniel Vassallo
我会尝试使用你所有的代码。实际上,我只使用了你在JS中编写的“haversine”公式。也许我哪里算错了。我会在尝试过你的代码后再联系你。 - Nordes
我找到了为什么会出现不精确的问题。实际上,在GMap的V3版本中,我们不再有返回polyLine长度(以公里或米为单位)的“getLength”函数。此外,如果我们只使用较短的线段,似乎是正确的,但当我们使用长线段(例如200公里对角线),我们可以看到线段和标记之间存在一些空隙。这是由于Haversine公式。该公式使用地球半径的“近似值”(6731公里)。 - Nordes
@Nordes:没错,就是这样。我认为 getLength() 函数也假设地球是一个球体,所以在 v2 演示中处理更大的距离时应该也会发生同样的情况。假设地球是一个球体可以使数学计算变得简单许多。 - Daniel Vassallo
非常棒的解释,而且附有代码也非常棒。谢谢! - Mahavirsinh Padhiyar

5

以下是所需函数的原型:

google.maps.Polygon.prototype.Distance = function() {
   var dist = 0;
   for (var i=1; i < this.getPath().getLength(); i++) {
      dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i-1));
   }
   return dist;
}

google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
    //var R = 6371; // km (change this constant to get miles)
    var R = 6378100; // meters
    var lat1 = this.lat();
    var lon1 = this.lng();
    var lat2 = newLatLng.lat();
    var lon2 = newLatLng.lng();
    var dLat = (lat2-lat1) * Math.PI / 180;
    var dLon = (lon2-lon1) * Math.PI / 180;
    var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
      Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) *
      Math.sin(dLon/2) * Math.sin(dLon/2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    var d = R * c;
    return d;
}

source


3
我使用了Martin Zeitler的方法来处理Google Map V3,目前一切运行良好。
 function init() {
       var mapOptions = {
            zoom: 15,
            center: new google.maps.LatLng(-6.208437004433984, 106.84543132781982),
            suppressInfoWindows: true,
                     };

        // Get all html elements for map
        var mapElement = document.getElementById('map1');

        // Create the Google Map using elements
        map = new google.maps.Map(mapElement, mapOptions);

        var nextMarkerAt = 0;     // Counter for the marker checkpoints.
        var nextPoint = null;     // The point where to place the next marker.


        while (true) {

            var routePoints = [ new google.maps.LatLng(47.656, -122.360),
                                new google.maps.LatLng(47.656, -122.343),
                                new google.maps.LatLng(47.690, -122.310),
                                new google.maps.LatLng(47.690, -122.270)];

                nextPoint = moveAlongPath(routePoints, nextMarkerAt);

            if (nextPoint) {
              //Adding marker from localhost
                MarkerIcon = "http://192.168.1.1/star.png";
                var marker = new google.maps.Marker
                    ({position: nextPoint,
                        map: map,
                        icon: MarkerIcon
                    });
                // Add +1000 meters for the next checkpoint.
                nextMarkerAt +=1000;

            }
            else {
                // moveAlongPath returned null, so there are no more check points.
                break;
            }
        }
 }


   Number.prototype.toRad = function () {
        return this * Math.PI / 180;
    }

    Number.prototype.toDeg = function () {
        return this * 180 / Math.PI;
    }

    function moveAlongPath(point, distance, index) {
        index = index || 0;  // Set index to 0 by default.

        var routePoints = [];

        for (var i = 0; i < point.length; i++) {
            routePoints.push(point[i]);
        }

        if (index < routePoints.length) {
            // There is still at least one point further from this point.

            // Construct a GPolyline to use the getLength() method.
            var polyline = new google.maps.Polyline({
                path: [routePoints[index], routePoints[index + 1]],
                strokeColor: '#FF0000',
                strokeOpacity: 0.8,
                strokeWeight: 2,
                fillColor: '#FF0000',
                fillOpacity: 0.35
            });

            // Get the distance from this point to the next point in the polyline.
            var distanceToNextPoint = polyline.Distance();

            if (distance <= distanceToNextPoint) {
                // distanceToNextPoint is within this point and the next.
                // Return the destination point with moveTowards().
                return moveTowards(routePoints, distance,index);
            }
            else {
                // The destination is further from the next point. Subtract
                // distanceToNextPoint from distance and continue recursively.
                return moveAlongPath(routePoints,
                    distance - distanceToNextPoint,
                    index + 1);
            }
        }
        else {
            // There are no further points. The distance exceeds the length
            // of the full path. Return null.
            return null;
        }
    }

    function moveTowards(point, distance,index) {

        var lat1 = point[index].lat.toRad();
        var lon1 = point[index].lng.toRad();
        var lat2 = point[index+1].lat.toRad();
        var lon2 = point[index+1].lng.toRad();
        var dLon = (point[index + 1].lng - point[index].lng).toRad();

        // Find the bearing from this point to the next.
        var brng = Math.atan2(Math.sin(dLon) * Math.cos(lat2),
            Math.cos(lat1) * Math.sin(lat2) -
            Math.sin(lat1) * Math.cos(lat2) *
            Math.cos(dLon));

        var angDist = distance / 6371000;  // Earth's radius.

        // Calculate the destination point, given the source and bearing.
        lat2 = Math.asin(Math.sin(lat1) * Math.cos(angDist) +
            Math.cos(lat1) * Math.sin(angDist) *
            Math.cos(brng));

        lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(angDist) *
            Math.cos(lat1),
            Math.cos(angDist) - Math.sin(lat1) *
            Math.sin(lat2));

        if (isNaN(lat2) || isNaN(lon2)) return null;



        return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
    }

    google.maps.Polyline.prototype.Distance = function () {
        var dist = 0;
        for (var i = 1; i < this.getPath().getLength(); i++) {
            dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i - 1));
        }
        return dist;
    }

    google.maps.LatLng.prototype.distanceFrom = function (newLatLng) {
        //var R = 6371; // km (change this constant to get miles)
        var R = 6378100; // meters
        var lat1 = this.lat();
        var lon1 = this.lng();
        var lat2 = newLatLng.lat();
        var lon2 = newLatLng.lng();
        var dLat = (lat2 - lat1) * Math.PI / 180;
        var dLon = (lon2 - lon1) * Math.PI / 180;
        var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
            Math.sin(dLon / 2) * Math.sin(dLon / 2);
        var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        var d = R * c;
        return d;
    }

大家好,有没有想法如何在react-google-map包装库上实现这个功能? - marvin lee jing rui

3

可能最好的方法是计算这些点的位置。

作为基本算法,您可以遍历 Polyline 中的所有点,并计算累积距离 - 如果下一个线段将您超过您的距离,则可以插值到达距离的点 - 然后为此在地图上添加一个感兴趣的点。


是的,那应该可行 - 我只是希望有某种狡猾的方法让API来做它 :) - mikl
@mikl 我可能是 masochist 说这话,但我认为像这样找出解决方案更有趣,其中没有明显的 API 方法。 - Rowland Shaw

0

我想将Daniel Vassalo的答案移植到iOS,但它没有正常工作,一些标记错位了,直到我进行了更改。

var dLon = (point.lng() - this.lng()).toRad();

var dLon = point.lng().toRad() - this.lng().toRad();

所以如果有人遇到标记错位的问题,可以尝试这个方法,或许会有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接