【问题标题】:How to add markers on Google Maps polylines based on distance along the line?如何根据沿线的距离在谷歌地图折线上添加标记?
【发布时间】:2011-02-11 11:46:19
【问题描述】:

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

【问题讨论】:

    标签: javascript google-maps distance intervals polyline


    【解决方案1】:

    几个月前,answered a similar problem 讨论了如何在 SQL Server 2008 的服务器端解决这个问题,我正在使用 Google Maps API v2 将相同的算法移植到 JavaScript。

    为了这个例子,让我们使用一条简单的 4 点折线,总长度约为 8,800 米。下面的 sn-p 将定义这条折线并将其渲染到地图上:

    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);
    

    现在在我们接近实际算法之前,我们需要一个函数,它在给定起点、终点和沿该线行进的距离时返回目的地点,幸运的是,有一些方便的 JavaScript 实现克里斯维内斯Calculate distance, bearing and more between Latitude/Longitude points

    特别是我已经从上述来源改编了以下两种方法来使用 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点数组,并且我们用这个点数组和2500米的距离调用我们的moveAlongPath()函数,它将在该路径上返回一个GLatLng距离第一个点 2.5 公里。

    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 的三个标记,您可以简单地这样做:

    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>
    

    以上示例的屏幕截图,每 1,000 米显示一个标记:

    【讨论】:

    • 我使用的是Google Map Api V3,你的公式看起来不错,但是当我放大到道路水平时,我可以看到google绘制的线和我的标记之间的距离。有什么理由会这样吗?
    • @Nordes:上面的例子会发生这种情况吗?我试图放大到最大缩放级别,并且标记似乎在线。截图:img408.imageshack.us/img408/8687/gmapnospace.png
    • 我会尝试使用您的所有代码。实际上,我只使用您在 JS 中制作的“haversine”公式。也许我在某个地方做错了。尝试使用您的代码后,我会尽快回复您。
    • 我发现了我为什么不准确。实际上在 GMap 的 V3 中,我们不再有函数“getLength”来返回折线的公里或米的长度。此外,如果我们保持小长度的线,这似乎是正确的,但是当我们做一条大线(对角线 200 公里)时,我们可以看到线和标记之间有一些空间。这是因为Haversine 公式。该公式使用地球半径(6731 公里)的“近似值”。
    • @Nordes:哦,是的,就是这样。我认为getLength() 函数也假定为球形地球,因此在距离较大的 v2 演示中也应该发生同样的情况。假设一个球形地球会使数学变得更简单。
    【解决方案2】:

    这些是所需功能的原型:

    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】:

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

      作为一种基本算法,您可以遍历折线中的所有点,并计算累积距离 - 如果下一段让您超过您的距离,您可以插入已达到距离的点 - 然后只需添加一个您的地图的兴趣点。

      【讨论】:

      • 是的,这应该是可行的——我只是希望有某种偷偷摸摸的方法可以让 API 做到这一点:)
      • @mikl 这么说我可能是个受虐狂,但我认为在没有明显 API 方法的情况下制定这样的解决方案会更有趣
      【解决方案4】:

      我已经使用 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 包装库上做到这一点吗?
      【解决方案5】:

      我想将 Daniel Vassalo's answer 移植到 iOS,但它无法正常工作,并且在我更改之前有些标记放错了位置

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

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

      因此,如果有人难以弄清楚为什么标记放错了位置,试试这个,也许它会有所帮助。

      【讨论】:

        猜你喜欢
        • 2019-08-21
        • 2020-12-12
        • 1970-01-01
        • 2021-08-12
        • 2021-03-29
        • 1970-01-01
        • 2019-03-07
        • 2019-08-02
        • 1970-01-01
        相关资源
        最近更新 更多