Кластеризация маркеров

При отображении большого количества маркеров на карте возникает несколько проблем:

  • снижается производительность браузера;
  • ухудшается читаемость карты;
  • маркеры перекрывают друг друга;
  • затрудняется взаимодействие с объектами в плотных областях.

Кластеризация маркеров представляет собой механизм объединения близко расположенных маркеров в один визуальный объект — кластер. Вместо отображения сотен или тысяч отдельных точек карта показывает компактные группы с числовым индикатором количества объектов.

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


Принцип работы кластеров

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

Без кластеризации карта выглядит перегруженной:

● ● ● ● ● ● ● ●
● ● ● ● ● ● ● ●
● ● ● ● ● ● ● ●

После кластеризации отображаются группы:

[120]   [85]

      [60]

[140]       [95]

Каждый кластер содержит информацию о количестве маркеров внутри него.

При увеличении масштаба:

[120]
   ↓

[50] [40] [30]

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


Библиотека MarkerClusterer

Для работы с кластерами в современных версиях Google Maps JavaScript API используется библиотека:

@googlemaps/markerclusterer

Установка через npm:

npm install @googlemaps/markerclusterer

Импорт:

import { MarkerClusterer } from "@googlemaps/markerclusterer";

При использовании через CDN:

<script
  src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js">
</script>

Создание карты

Базовая настройка карты:

const map = new google.maps.Map(
  document.getElementById("map"),
  {
    center: { lat: 55.7558, lng: 37.6176 },
    zoom: 10
  }
);

Создание массива маркеров

Кластеризатор работает с коллекцией объектов Marker.

Пример создания массива:

const locations = [
  { lat: 55.751, lng: 37.618 },
  { lat: 55.752, lng: 37.619 },
  { lat: 55.753, lng: 37.620 },
  { lat: 55.754, lng: 37.621 }
];

const markers = locations.map(location => {
  return new google.maps.Marker({
    position: location
  });
});

Обратите внимание, что маркеры не обязательно сразу добавлять на карту.


Создание кластеризатора

После подготовки массива маркеров создаётся экземпляр MarkerClusterer.

const markerCluster = new MarkerClusterer({
  map,
  markers
});

Полный пример:

const map = new google.maps.Map(
  document.getElementById("map"),
  {
    center: { lat: 55.7558, lng: 37.6176 },
    zoom: 10
  }
);

const markers = locations.map(position => {
  return new google.maps.Marker({
    position
  });
});

new MarkerClusterer({
  map,
  markers
});

После выполнения кода близкие точки автоматически объединяются в группы.


Генерация большого количества маркеров

На практике координаты часто поступают из базы данных или внешнего API.

Для тестирования можно создать случайные точки.

const markers = [];

for (let i = 0; i < 1000; i++) {
  const marker = new google.maps.Marker({
    position: {
      lat: 55.75 + (Math.random() - 0.5) * 0.3,
      lng: 37.61 + (Math.random() - 0.5) * 0.3
    }
  });

  markers.push(marker);
}

new MarkerClusterer({
  map,
  markers
});

Даже при тысяче объектов производительность остаётся значительно выше по сравнению с отображением каждого маркера отдельно.


Алгоритмы кластеризации

Современная библиотека поддерживает различные алгоритмы формирования кластеров.

По умолчанию используется:

SuperClusterAlgorithm

Он основан на пространственной индексации и эффективно работает с десятками тысяч точек.

Настройка алгоритма:

import {
  MarkerClusterer,
  SuperClusterAlgorithm
} from "@googlemaps/markerclusterer";

const clusterer = new MarkerClusterer({
  map,
  markers,
  algorithm: new SuperClusterAlgorithm()
});

Настройка радиуса кластеризации

Одним из важнейших параметров является радиус объединения.

const clusterer = new MarkerClusterer({
  map,
  markers,
  algorithm: new SuperClusterAlgorithm({
    radius: 100
  })
});

Параметр:

radius

определяет расстояние в пикселях, внутри которого точки считаются частью одного кластера.

Малый радиус

radius: 40

Особенности:

  • больше отдельных маркеров;
  • больше кластеров;
  • более детальная карта.

Большой радиус

radius: 120

Особенности:

  • меньше кластеров;
  • более агрессивное объединение;
  • лучшая производительность.

Ограничение максимального масштаба

Иногда требуется отключить кластеризацию после определённого уровня приближения.

const clusterer = new MarkerClusterer({
  map,
  markers,
  algorithm: new SuperClusterAlgorithm({
    maxZoom: 15
  })
});

После достижения масштаба 15 и выше пользователь увидит отдельные маркеры.


Динамическое добавление маркеров

Кластеризатор позволяет добавлять объекты после создания.

Добавление одного маркера:

const marker = new google.maps.Marker({
  position: {
    lat: 55.76,
    lng: 37.63
  }
});

clusterer.addMarker(marker);

Добавление массива:

clusterer.addMarkers([
  marker1,
  marker2,
  marker3
]);

Удаление маркеров

Удаление конкретного объекта:

clusterer.removeMarker(marker);

Удаление нескольких объектов:

clusterer.removeMarkers([
  marker1,
  marker2
]);

Полная очистка:

clusterer.clearMarkers();

Обновление кластеров

После изменения набора маркеров иногда требуется пересчитать кластеры.

clusterer.render();

Метод инициирует повторное построение всех групп.


Кастомизация внешнего вида кластеров

По умолчанию используются стандартные круглые значки.

Для полного контроля применяется собственный рендерер.

const renderer = {
  render({ count, position }) {
    return new google.maps.Marker({
      position,
      label: {
        text: String(count),
        color: "#ffffff",
        fontSize: "14px"
      }
    });
  }
};

Подключение:

new MarkerClusterer({
  map,
  markers,
  renderer
});

Создание собственного SVG-кластера

Современный подход предполагает использование SVG.

const renderer = {
  render({ count, position }) {

    const svg = `
      <svg width="60" height="60"
           xmlns="http://www.w3.org/2000/svg">
        <circle
          cx="30"
          cy="30"
          r="25"
          fill="#1976d2"/>
        <text
          x="30"
          y="35"
          text-anchor="middle"
          fill="white"
          font-size="16">
          ${count}
        </text>
      </svg>
    `;

    return new google.maps.Marker({
      position,
      icon: {
        url:
          "data:image/svg+xml;base64," +
          btoa(svg),
        scaledSize:
          new google.maps.Size(60, 60)
      }
    });
  }
};

Преимущества SVG:

  • масштабируемость;
  • минимальный размер ресурсов;
  • гибкая настройка дизайна;
  • высокая чёткость на Retina-дисплеях.

Использование разных размеров кластеров

Количество объектов внутри кластера может влиять на его внешний вид.

render({ count, position }) {

  let size = 40;

  if (count > 50) {
    size = 60;
  }

  if (count > 200) {
    size = 80;
  }

  return new google.maps.Marker({
    position,
    icon: {
      url: createClusterIcon(size, count),
      scaledSize:
        new google.maps.Size(size, size)
    }
  });
}

Подобный подход позволяет визуально выделять наиболее плотные скопления объектов.


Обработка клика по кластеру

Кластер может реагировать на действия пользователя.

Пример увеличения масштаба:

clusterer.addListener(
  "click",
  event => {
    map.setZoom(
      map.getZoom() + 2
    );

    map.panTo(
      event.latLng
    );
  }
);

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


Получение информации о кластере

В обработчике доступны данные о группе маркеров.

clusterer.addListener(
  "click",
  ({ cluster }) => {

    console.log(
      cluster.markers.length
    );

  }
);

Результат:

153

Количество объектов внутри выбранного кластера.


Работа с AdvancedMarkerElement

В новых проектах рекомендуется использовать современный класс:

google.maps.marker.AdvancedMarkerElement

Создание маркеров:

const markers = locations.map(position => {

  return new google.maps.marker
    .AdvancedMarkerElement({
      position
    });

});

Кластеризатор поддерживает работу с такими объектами аналогично обычным маркерам.


Кластеризация объектов недвижимости

Типичный пример применения — карта недвижимости.

Структура данных:

[
  {
    id: 1,
    price: 120000,
    lat: 55.75,
    lng: 37.61
  },
  {
    id: 2,
    price: 150000,
    lat: 55.74,
    lng: 37.63
  }
]

Создание маркеров:

const markers = properties.map(item => {
  return new google.maps.Marker({
    position: {
      lat: item.lat,
      lng: item.lng
    }
  });
});

Затем все объекты объединяются:

new MarkerClusterer({
  map,
  markers
});

На небольшом масштабе пользователь видит распределение объектов по районам, а при увеличении получает доступ к отдельным предложениям.


Кластеризация точек доставки

Логистические системы часто работают с тысячами адресов.

Преимущества кластеризации:

  • снижение нагрузки на карту;
  • визуальное определение плотности заказов;
  • упрощение анализа маршрутов;
  • удобное отображение регионов обслуживания.

Для карт служб доставки кластеризация является практически обязательным инструментом.


Производительность при большом количестве объектов

Рекомендации при работе с десятками тысяч маркеров:

Использование SuperClusterAlgorithm

algorithm: new SuperClusterAlgorithm()

Наиболее эффективный вариант для крупных наборов данных.

Ленивое получение данных

Вместо загрузки всех точек:

100000 объектов

целесообразно получать данные только для текущей области карты.

map.addListener("idle", loadMarkers);

Ограничение детализации

maxZoom: 14

Позволяет избежать отображения огромного количества объектов одновременно.

Использование AdvancedMarkerElement

Современные маркеры лучше интегрированы с текущими версиями Google Maps API и обеспечивают более гибкую настройку интерфейса.


Типичные ошибки

Добавление маркеров сразу на карту

Нежелательный вариант:

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

Затем тот же объект передаётся кластеризатору.

Это приводит к лишним операциям отображения.

Предпочтительно:

new google.maps.Marker({
  position
});

После чего передать массив в кластеризатор.


Слишком большой радиус

radius: 300

В результате карта теряет детализацию и объединяет удалённые объекты.


Отсутствие ограничения данных

Даже кластеризация не решает проблему бесконечного роста объёма данных. При работе с очень крупными наборами рекомендуется серверная фильтрация по текущему окну карты.


Частое пересоздание кластеризатора

Нежелательно создавать новый экземпляр при каждом обновлении данных:

new MarkerClusterer(...)

Гораздо эффективнее обновлять существующий объект:

clusterer.clearMarkers();
clusterer.addMarkers(markers);
clusterer.render();

Архитектура работы кластеризации

Типичная последовательность выглядит следующим образом:

Источник данных
        ↓
Получение координат
        ↓
Создание маркеров
        ↓
MarkerClusterer
        ↓
Алгоритм группировки
        ↓
Кластеры
        ↓
Отображение на карте

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