Как добавить маркеры на полилинии Карт Google в зависимости от расстояния вдоль линии?

Я пытаюсь создать карту Google, где пользователь может построить маршрут, по которому он шел / бегал / катался на велосипеде, и посмотреть, как долго он бежал. GPolyline класс с этим getLength() Метод очень полезен в этом отношении (по крайней мере, для Google Maps API V2), но я хотел добавить маркеры, основанные на расстоянии, например, маркер для 1 км, 5 км, 10 км и т. д., но, похоже, что есть нет очевидного способа найти точку на полилинии, основанную на том, как далеко вдоль линии она находится. Какие-либо предложения?

5 ответов

Решение

Пару месяцев назад, ответив на аналогичную проблему, касающуюся решения этой проблемы на стороне сервера в SQL Server 2008, я переношу тот же алгоритм на JavaScript с помощью API Карт Google v2.

Для примера рассмотрим простую 4-точечную полилинию общей длиной около 8 800 метров. Фрагмент ниже определит эту ломаную линию и отобразит ее на карте:

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 Криса Венесса в Рассчитать расстояние, направление и многое другое между точками широты и долготы.

В частности, я адаптировал следующие два метода из вышеупомянутого источника для работы с 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() метод до текущей точки, пройдя следующую точку и пройденное расстояние. Вернуть результат и прервать итерацию.

    Else:

    ... точка назначения находится дальше по пути от следующей точки итерации. Нам нужно вычесть расстояние между этой точкой и следующей точкой из общего расстояния, чтобы пройти вдоль пути. Продолжите итерацию с измененным расстоянием.

Возможно, вы заметили, что мы можем легко реализовать вышеизложенное, а не итеративно. Итак, давайте сделаем это:

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 очки, и мы вызываем наши moveAlongPath() функция с этим массивом точек и на расстоянии 2500 метров, он вернет 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() для каждой контрольной точки нам нужно на пути. Если вам нужны три маркера на 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(),

Мы можем собрать это вместе для полной реализации. В этом примере мы сбрасываем маркер каждые 1000 метров вдоль пути 8,8 км, определенного ранее:

<!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 - переместить точку вдоль пути

Я узнал, почему у меня была неточность. На самом деле в V3 GMap у нас больше нет функции "getLength", которая возвращает длину в Км или в метрах Полилинии.

Вот прототипы для требуемой функции - надеюсь, это поможет в дальнейшем:

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

источник

Возможно, лучшим подходом было бы рассчитать, где находятся эти точки.

В качестве базового алгоритма вы можете перебрать все точки полилинии и рассчитать кумулятивное расстояние - если следующий отрезок поместит вас на расстояние, вы можете интерполировать точку, в которой было достигнуто расстояние, - затем просто добавить интересующую точку для вашей карты для этого.

Я использовал метод Мартина Цайтлера для работы с 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;
    }

Я хотел перенести ответ Даниэля Вассало на iOS, но он не работал должным образом, и некоторые маркеры были не на своем месте, пока я не изменился

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

в

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

Так что если у кого-то возникнут проблемы с выяснением, почему маркеры неуместны, попробуйте это, и, возможно, это поможет.

Другие вопросы по тегам