Несколько кругов -> Один полигон?

Используя Google Maps API версии 3, я смог создать несколько объектов google.maps.Circle на своей карте. Однако теперь мне нужно их как-то «связать». У меня есть следующая карта с несколькими кругами:

Карта с несколькими кругами

Теперь мне нужно, чтобы это выглядело примерно так:

Правильная карта
(источник: pcwp .com)

Искал решения по всему интернету, но безрезультатно. Любые идеи?


person Josh Delsman    schedule 10.04.2010    source источник
comment
Кстати, мне было бы интересно посмотреть, как это получится, если у вас есть возможность дать ссылку на то, что вы делаете.   -  person Matti Virkkunen    schedule 11.04.2010


Ответы (2)


Возможно, вы захотите решить эту проблему, добавив дополнительные круги с интервалом x с увеличением радиуса между каждой точкой пути. Это было бы очень легко реализовать и будет работать для любого направления циклона. Очевидно, что предложенное Матти решение для создания многоугольника путем соединения всех касательных было бы более точно, но вы можете рассмотреть это как возможную альтернативу. Основным недостатком является то, что могут потребоваться некоторые усилия, чтобы он выглядел красиво, и, очевидно, он будет использовать больше ресурсов на стороне клиента, чем если бы вы визуализировали один полигон.

Давайте начнем с воссоздания вашей карты:

<!DOCTYPE html>
<html> 
<head> 
   <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 
   <title>Google Maps Cyclones</title> 
   <script src="http://maps.google.com/maps/api/js?sensor=false" 
           type="text/javascript"></script> 
</head> 
<body> 
   <div id="map" style="width: 600px; height: 400px"></div> 

   <script type="text/javascript"> 
      var i;

      var mapOptions = { 
         mapTypeId: google.maps.MapTypeId.TERRAIN,
         center: new google.maps.LatLng(28.50, -81.50),
         zoom: 5
      };

      var map = new google.maps.Map(document.getElementById("map"), 
                                    mapOptions);

      var pathPoints = [
         new google.maps.LatLng(25.48, -71.26),
         new google.maps.LatLng(25.38, -73.70),
         new google.maps.LatLng(25.28, -77.00),
         new google.maps.LatLng(25.24, -80.11),
         new google.maps.LatLng(25.94, -82.71),
         new google.maps.LatLng(27.70, -87.14)
      ];

      pathPoints[0].radius = 80;
      pathPoints[1].radius = 100;
      pathPoints[2].radius = 200;
      pathPoints[3].radius = 300;
      pathPoints[4].radius = 350;
      pathPoints[5].radius = 550;

      new google.maps.Polyline({
         path: pathPoints,
         strokeColor: '#00FF00',
         strokeOpacity: 1.0,
         strokeWeight: 3,
         map: map
      });

      for (i = 0; i < pathPoints.length; i++) {
         new google.maps.Circle({
            center: pathPoints[i],
            radius: pathPoints[i].radius * 1000,
            fillColor: '#FF0000',
            fillOpacity: 0.2,
            strokeOpacity: 0.5,
            strokeWeight: 1, 
            map: map
         });
      }

   </script> 
</body> 
</html>

http://img186.imageshack.us/img186/1197/mp1h.png

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

Прежде чем мы продолжим, нам нужно определить несколько методов, чтобы иметь возможность вычислять расстояние и пеленг от одной точки к другой. Нам также понадобится метод, который будет возвращать точку назначения при задании пеленга и пройденного расстояния от исходной точки. К счастью, есть очень хорошая реализация этих методов на JavaScript от Криса Венесса по адресу Рассчитать расстояние. , азимут и многое другое между точками широты/долготы. Следующие методы были адаптированы для работы с google.maps.LatLng Google:

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

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

google.maps.LatLng.prototype.destinationPoint = function(brng, dist) {
   dist = dist / 6371;  
   brng = brng.toRad();  
   var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();

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

   if (isNaN(lat2) || isNaN(lon2)) return null;
   return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
}

google.maps.LatLng.prototype.bearingTo = function(point) {
   var lat1 = this.lat().toRad(), lat2 = point.lat().toRad();
   var dLon = (point.lng()-this.lng()).toRad();

   var y = Math.sin(dLon) * Math.cos(lat2);
   var x = Math.cos(lat1)*Math.sin(lat2) -
           Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);

   var brng = Math.atan2(y, x);

   return ((brng.toDeg()+360) % 360);
}

google.maps.LatLng.prototype.distanceTo = function(point) {
   var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
   var lat2 = point.lat().toRad(), lon2 = point.lng().toRad();
   var dLat = lat2 - lat1;
   var dLon = lon2 - lon1;

   var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
           Math.cos(lat1) * Math.cos(lat2) * 
           Math.sin(dLon/2) * Math.sin(dLon/2);

   return 6371 * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
}

Затем нам нужно будет добавить еще один цикл, который рендерит промежуточные круги внутри цикла for, который мы использовали ранее для рендеринга исходных кругов. Вот как это можно реализовать:

var distanceStep = 50;    // Render an intermediate circle every 50km.

for (i = 0; i < pathPoints.length; i++) {
   new google.maps.Circle({
      center: pathPoints[i],
      radius: pathPoints[i].radius * 1000,
      fillColor: '#FF0000',
      fillOpacity: 0.2,
      strokeOpacity: 0.5,
      strokeWeight: 1, 
      map: map
   });

   if (i < (pathPoints.length - 1)) {
      distanceToNextPoint = pathPoints[i].distanceTo(pathPoints[i + 1]);
      bearingToNextPoint = pathPoints[i].bearingTo(pathPoints[i + 1]);
      radius = pathPoints[i].radius;
      radiusIncrement = (pathPoints[i + 1].radius - radius) / 
                        (distanceToNextPoint / distanceStep);

      for (j = distanceStep; 
           j < distanceToNextPoint; 
           j += distanceStep, radius += radiusIncrement) {

         new google.maps.Circle({
            center: pathPoints[i].destinationPoint(bearingToNextPoint, j),
            radius: radius * 1000,
            fillColor: '#FF0000',
            fillOpacity: 0.2,
            strokeWeight: 0,
            map: map
         });
      }
   }
}

Вот что мы бы получили:

Циклоны на Google Картах – рис. 2

А вот как это выглядело бы без черной обводки вокруг исходных кругов:

http://img181.imageshack.us/img181/2908/mp3t.png

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

В любом случае ниже приведена полная реализация этого примера:

<!DOCTYPE html>
<html> 
<head> 
   <meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 
   <title>Google Maps Cyclones</title> 
   <script src="http://maps.google.com/maps/api/js?sensor=false" 
           type="text/javascript"></script> 
</head> 
<body> 
   <div id="map" style="width: 600px; height: 400px"></div> 

   <script type="text/javascript"> 
      Number.prototype.toRad = function() {
         return this * Math.PI / 180;
      }

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

      google.maps.LatLng.prototype.destinationPoint = function(brng, dist) {
         dist = dist / 6371;  
         brng = brng.toRad();  
         var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();

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

         if (isNaN(lat2) || isNaN(lon2)) return null;
         return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
      }

      google.maps.LatLng.prototype.bearingTo = function(point) {
         var lat1 = this.lat().toRad(), lat2 = point.lat().toRad();
         var dLon = (point.lng()-this.lng()).toRad();

         var y = Math.sin(dLon) * Math.cos(lat2);
         var x = Math.cos(lat1)*Math.sin(lat2) -
                 Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);

         var brng = Math.atan2(y, x);

         return ((brng.toDeg()+360) % 360);
      }

      google.maps.LatLng.prototype.distanceTo = function(point) {
         var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
         var lat2 = point.lat().toRad(), lon2 = point.lng().toRad();
         var dLat = lat2 - lat1;
         var dLon = lon2 - lon1;

         var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                 Math.cos(lat1) * Math.cos(lat2) * 
                 Math.sin(dLon/2) * Math.sin(dLon/2);

         return 6371 * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
      }

      var i;
      var j;
      var distanceToNextPoint;
      var bearingToNextPoint;      
      var radius;
      var radiusIncrement;
      var distanceStep = 50;    // Render an intermediate circle every 50km.

      var mapOptions = { 
         mapTypeId: google.maps.MapTypeId.TERRAIN,
         center: new google.maps.LatLng(28.50, -81.50),
         zoom: 5
      };

      var map = new google.maps.Map(document.getElementById("map"), mapOptions);

      var pathPoints = [
         new google.maps.LatLng(25.48, -71.26),
         new google.maps.LatLng(25.38, -73.70),
         new google.maps.LatLng(25.28, -77.00),
         new google.maps.LatLng(25.24, -80.11),
         new google.maps.LatLng(25.94, -82.71),
         new google.maps.LatLng(27.70, -87.14)
      ];

      pathPoints[0].radius = 80;
      pathPoints[1].radius = 100;
      pathPoints[2].radius = 200;
      pathPoints[3].radius = 300;
      pathPoints[4].radius = 350;
      pathPoints[5].radius = 550;

      new google.maps.Polyline({
         path: pathPoints,
         strokeColor: '#00FF00',
         strokeOpacity: 1.0,
         strokeWeight: 3,
         map: map
      });

      for (i = 0; i < pathPoints.length; i++) {
         new google.maps.Circle({
            center: pathPoints[i],
            radius: pathPoints[i].radius * 1000,
            fillColor: '#FF0000',
            fillOpacity: 0.2,
            strokeOpacity: 0.5,
            strokeWeight: 0, 
            map: map
         });

         if (i < (pathPoints.length - 1)) {
            distanceToNextPoint = pathPoints[i].distanceTo(pathPoints[i + 1]);
            bearingToNextPoint = pathPoints[i].bearingTo(pathPoints[i + 1]);
            radius = pathPoints[i].radius;
            radiusIncrement = (pathPoints[i + 1].radius - radius) / 
                              (distanceToNextPoint / distanceStep);

            for (j = distanceStep; 
                 j < distanceToNextPoint; 
                 j += distanceStep, radius += radiusIncrement) {

               new google.maps.Circle({
                  center: pathPoints[i].destinationPoint(bearingToNextPoint, j),
                  radius: radius * 1000,
                  fillColor: '#FF0000',
                  fillOpacity: 0.2,
                  strokeWeight: 0,
                  map: map
               });
            }
         }
      }

   </script> 
</body> 
</html>
person Daniel Vassallo    schedule 11.04.2010
comment
Вау, хороший ответ. Мне тоже придется попробовать реализовать свой, поэтому я могу опубликовать код (будет хорошим упражнением для моих навыков ржавой геометрии) - person Matti Virkkunen; 13.04.2010
comment
@Matti: Я с нетерпением жду вашей попытки :) ... Честно говоря, я собирался сначала попытаться реализовать ваш подход tangets, но математика стала слишком сложной в 2 часа ночи!... Почувствуйте можете повторно использовать любую часть кода моего ответа, если вы действительно решите сделать решающий шаг. - person Daniel Vassallo; 13.04.2010
comment
Имейте в виду, что на каждом из шести отдельных кругов есть ошибка прогноза. К сожалению, я знаю только ошибку прогноза в этих точках, так что это должно быть среднее значение по этим дополнительным 50-километровым кругам. - person Josh Delsman; 25.04.2010

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

Это может сломаться, если ваша цепочка кругов не такая аккуратная, как в первом посте (много перекрытий, круги внутри кругов и т. д.), но это только начало.

person Matti Virkkunen    schedule 10.04.2010
comment
Иногда могут быть круги, движущиеся с востока на запад, иногда с севера на юг, а иногда круги могут быть очень близкими, если циклон не движется очень быстро в течение прогнозируемого периода. Также могут быть случаи, когда имеется очень четко выраженная рекривизна, когда она почти соответствует С-образной форме. Будет ли это работать? Если да, то я начну! - person Josh Delsman; 11.04.2010
comment
Он должен работать. Если у вас возникнут проблемы со слишком большим количеством кругов, вы всегда можете отказаться от некоторых. - person Matti Virkkunen; 11.04.2010
comment
Я также хотел бы отметить, что будут моменты, когда пересекающихся линий нет, поэтому их будет невозможно найти (см. первое изображение) - person Josh Delsman; 11.04.2010
comment
Я говорю не о пересечениях самих окружностей, а о их касательных. - person Matti Virkkunen; 11.04.2010
comment
@Matti: я даже не заметил, что он рассказал вам, как его найти на самом деле. Я просто дал ссылку на него как на пример того, о чем вы говорили. Надеюсь, я не сильно испортил ему удовольствие :-) - person Matthew Crumley; 11.04.2010
comment
@ Мэтью: Ну, я думаю, это не слишком портит. - person Matti Virkkunen; 11.04.2010