В последнее время, работая с Google Maps, нашей команде нужно было иметь возможность размещать маркеры карты внутри различных типов фигур, нарисованных на карте. Это просто для кругов и прямоугольников, поскольку каждый из них имеет определенную центральную точку. Однако для более сложных многоугольников «разумное» место для размещения маркера не обязательно является очевидным или напрямую предоставляется API Карт Google. Это побудило нас искать легкое решение, которое могло бы найти достаточно подходящее место внутри многоугольника для установки маркера, предпочтительно с использованием существующих геометрических функций Google Maps.

Настраивать

Как обычно с проектами, использующими Google Maps API, нам нужно включить тег скрипта в наш HTML-документ, чтобы браузер получил код JavaScript из Google. В дополнение к обычному ключу API, который требуется для API Карт, нам также необходимо загрузить версию скрипта, содержащую дополнительную библиотеку геометрии, поскольку нам потребуются некоторые функции из «google.maps.geometry». Достигается это так:

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_MAPS_API_KEY_HERE&libraries=geometry&callback=fnToRunWhenAPILoaded" async defer></script>

Приближение к центральной точке многоугольника

С помощью Google Maps API легко получить центральную точку прямоугольника или круга и разместить там маркер. Например, если «прямоугольник» - это «google.maps.Rectangle»:

new google.maps.Marker({
  position: rectangle.getBounds().getCenter(),
  map: map
});

Однако для более сложных многоугольников, представленных google.maps.Polygon, этого не существует, поэтому:

polygon.getBounds();

не работает во время выполнения с:

Uncaught TypeError: polygon.getBounds is not a function

Для прямоугольных объектов функция getBounds возвращает минимальную ограничивающую рамку для прямоугольника (наименьшую рамку, содержащую все точки внутри прямоугольника, которая, конечно же, является самим прямоугольником). Мы начали смотреть, как реализовать функцию, подобную getBounds, для многоугольников, и быстро нашли изящное решение на Stack Overflow от пользователя furiozo:

google.maps.Polygon.prototype.getBoundingBox = function() {
  var bounds = new google.maps.LatLngBounds();
  this.getPath().forEach(function(element,index) {
    bounds.extend(element)
  });
  return(bounds);
};

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

Поскольку объект «google.maps.LatLngBounds» имеет «getCenter», теперь мы можем поместить наш маркер в центр минимального ограничивающего прямоугольника, который полностью содержит многоугольник:

var polygon = new google.maps.Polygon({
  paths: [
    { lat: 41.78500, lng: -87.75133 },
    { lat: 41.77681, lng: -87.87836 },
    { lat: 41.80138, lng: -87.92780 },
    { lat: 41.77988, lng: -87.95527 },
    { lat: 41.83208, lng: -87.95801 },
    { lat: 41.83208, lng: -87.94154 },
    { lat: 41.81673, lng: -87.88866 },
    { lat: 41.81417, lng: -87.78773 },
    { lat: 41.87607, lng: -87.77056 },
    { lat: 41.78500, lng: -87.75133 }
  ],
  strokeColor: '#FF0000',
  strokeOpacity: 0.8,
  strokeWeight: 2,
  fillColor: '#FF0000',
  fillOpacity: 0.35
});
new google.maps.Marker({
  position: polygon.getBoundingBox().getCenter(),
  map: map
});

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

В этом случае точная центральная точка ограничивающей рамки (показанная черным прямоугольником) находится за пределами многоугольника. Хотя это не всегда так, нам явно нужно поработать здесь…

Маркер всегда падает внутри многоугольника

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

  • Если центральная точка ограничивающего прямоугольника находится внутри многоугольника, поместите маркер туда.
  • В противном случае точки выборки находятся в фиксированных процентах от высоты ограничивающего прямоугольника к северу и югу от центральной точки.
  • Выборочные точки с фиксированным процентным соотношением ширины ограничивающей рамки к востоку и западу от центральной точки
  • Остановитесь, когда мы найдем первую точку, которая находится внутри области многоугольника, и поместите туда маркер.
  • Оберните эту логику как функцию «getApproximateCenter» и добавьте ее в прототип «google.maps.Polygon».

Google Maps API позволяет относительно легко реализовать эту стратегию (также с минимальными математическими вычислениями, поскольку мы используем их библиотеку геометрии).

google.maps.Polygon.prototype.getApproximateCenter = function() {
  var boundsHeight = 0,
      boundsWidth = 0,
      centerPoint,
      heightIncr = 0,
      maxSearchLoops,
      maxSearchSteps = 10,
      n = 1,
      northWest,
      polygonBounds = this.getBoundingBox(),
      testPos,
      widthIncr = 0;
  // Get polygon Centroid
  centerPoint = polygonBounds.getCenter();
  if (google.maps.geometry.poly.containsLocation(centerPoint, this))
  {
    // Nothing to do Centroid is in polygon use it as is
    return centerPoint;
  } else {
    maxSearchLoops = maxSearchSteps / 2;
    // Calculate NorthWest point so we can work out 
    // height of polygon NW->SE
    northWest = new google.maps.LatLng(
      polygonBounds.getNorthEast().lat(),
      polygonBounds.getSouthWest().lng()
    );
    // Work out how tall and wide the bounds are and what our search
    // increment will be
    boundsHeight =
      google.maps.geometry.spherical.computeDistanceBetween(
        northWest,
        polygonBounds.getSouthWest()
      );
    heightIncr = boundsHeight / maxSearchSteps;
    boundsWidth = 
      google.maps.geometry.spherical.computeDistanceBetween(
        northWest,
        polygonBounds.getNorthEast()
      );
    widthIncr = boundsWidth / maxSearchSteps;
    // Expand out from Centroid and find a point within polygon at
    // 0, 90, 180, 270 degrees
    for (; n <= maxSearchSteps; n++) {
      // Test point North of Centroid
      testPos = google.maps.geometry.spherical.computeOffset(
        centerPoint,
        (heightIncr * n),
        0
      );
      if (google.maps.geometry.poly.containsLocation(
        testPos, 
        this)
      ) {
        break;
      }
      // Test point East of Centroid
      testPos = google.maps.geometry.spherical.computeOffset(
        centerPoint,
        (widthIncr * n),
        90
      );
      if (google.maps.geometry.poly.containsLocation(
        testPos, 
        this)
      ) {
        break;
      }
      // Test point South of Centroid
      testPos = google.maps.geometry.spherical.computeOffset(
        centerPoint,
        (heightIncr * n),
        180
      );
      if (google.maps.geometry.poly.containsLocation(
        testPos, 
        this)
      ) {
        break;
      }
      // Test point West of Centroid
      testPos = google.maps.geometry.spherical.computeOffset(
        centerPoint,
        (widthIncr * n),
        270
      );
      if (google.maps.geometry.poly.containsLocation(
        testPos, 
        this)
      ) {
        break;
      }
    }
    return(testPos);
  }
};

Здесь мы используем google.maps.geometry.poly.containsLocation, чтобы определить, находится ли центральная точка ограничивающей рамки внутри многоугольника. Если нет, мы затем вычисляем высоту и ширину ограничивающего прямоугольника и делим каждый на количество шагов выборки, которые мы хотим предпринять, а затем зацикливаемся на проверке точек на увеличивающихся расстояниях Север / Восток / Юг / Запад, пока не найдем ту, которая попадает внутрь многоугольник. Для этого мы используем:

google.maps.geometry.spherical.computeOffset(centerPoint, distance, bearing)

чтобы вычислить местоположение следующей точки, которую нужно попробовать, вместе с:

google.maps.geometry.poly.containsLocation(pointToTest, polygon)

который сообщает нам, находится ли эта точка внутри многоугольника или нет.

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

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

Тестирование

Итак, чтобы разместить маркер, теперь мы можем просто использовать:

new google.maps.Marker({
  position: polygon.getApproximateCenter(),
  map: map
});

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

Мы создали полный пример, который вы можете попробовать сами, и вы можете найти его здесь, на GitHub. Вам просто нужно получить ключ API Карт Google, который доступен здесь.

Альтернативные решения

Наше очень простое решение, которое выполняет то, что нам нужно от него для нашего варианта использования (наши пользователи также могли перемещать маркеры вручную в положение, которое им нравится, и нам нужно было только показать один многоугольник на карте в любой момент времени) . Он также привязан к Google Maps, поскольку в этом случае нам не пришлось рассматривать другие решения для картографии. Мы могли бы дополнительно улучшить его для поиска в большем количестве направлений (например, NE, SE, SW, NW). Это может привести к немного лучшему результату, и это необходимо компенсировать увеличением требуемых вычислительных затрат.

Однако могут быть случаи, когда вам понадобится более точная центральная точка, чем эта, или что-то, что не зависит от Google Maps. Команда MapBox недавно написала сообщение в блоге о разработанном ими алгоритме и предлагает библиотеку, которая находит точку внутри многоугольника, наиболее удаленную от края, с помощью рекурсивного алгоритма, без зависимости от Google Maps.

Повеселись!

Если вы знаете кого-то, кому эта статья покажется информативной, поделитесь, пожалуйста! И подписывайтесь на меня в Twitter или Modus Create: Front End Development.