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

Назначение кластеризации и область применения

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

Кластеризация решает эту задачу за счёт объединения близко расположенных точек в один объект, который отображает агрегированную информацию. В зависимости от масштаба карты такие группы динамически изменяются: при отдалении точки объединяются, при приближении — разделяются.

Ключевые задачи кластеризации:

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

Базовая архитектура кластеризации в OpenLayers

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

  • ol/source/Vector — исходный набор точек;
  • ol/source/Cluster — обёртка над векторным источником, выполняющая группировку;
  • ol/layer/Vector — слой для отображения кластеров.

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


Создание базового источника точек

Перед включением кластеризации формируется обычный векторный источник:

import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';

const features = [];

for (let i = 0; i < 1000; i++) {
  const feature = new Feature({
    geometry: new Point([
      Math.random() * 10000000,
      Math.random() * 10000000
    ])
  });

  features.push(feature);
}

const vectorSource = new VectorSource({
  features: features
});

Этот источник содержит «сырые» точки без группировки.


Подключение Cluster-источника

Кластеризация создаётся поверх существующего источника:

import Cluster from 'ol/source/Cluster';

const clusterSource = new Cluster({
  distance: 40,
  source: vectorSource
});

Параметр distance определяет радиус кластеризации в пикселях. Чем больше значение, тем агрессивнее объединение объектов.

Поведение параметра:

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

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

Кластеры отображаются как обычный векторный слой:

import VectorLayer from 'ol/layer/Vector';

const clusterLayer = new VectorLayer({
  source: clusterSource,
  style: function (feature) {
    const size = feature.get('features').length;

    return new Style({
      image: new CircleStyle({
        radius: 10 + Math.log(size),
        fill: new Fill({
          color: 'rgba(0, 150, 255, 0.6)'
        }),
        stroke: new Stroke({
          color: '#3399CC',
          width: 1
        })
      }),
      text: new Text({
        text: size.toString(),
        fill: new Fill({
          color: '#fff'
        })
      })
    });
  }
});

Особенность кластерного объекта заключается в том, что он содержит массив исходных объектов:

feature.get('features')

Именно этот массив используется для определения размера кластера и его визуализации.


Структура кластерного объекта

Каждый кластер представляет собой Feature, внутри которого хранится:

  • массив исходных объектов;
  • общая геометрия (центр кластера);
  • дополнительные вычисляемые свойства.

Пример доступа к данным:

const originalFeatures = clusterFeature.get('features');
const count = originalFeatures.length;

При необходимости можно извлечь исходные координаты:

originalFeatures.forEach(f => {
  const coord = f.getGeometry().getCoordinates();
});

Динамическое изменение стиля кластеров

Стиль кластеров часто зависит от их размера. Это позволяет визуально различать плотность данных.

Пример градации:

style: function (feature) {
  const size = feature.get('features').length;

  let color = 'green';

  if (size > 50) {
    color = 'red';
  } else if (size > 20) {
    color = 'orange';
  } else if (size > 5) {
    color = 'yellow';
  }

  return new Style({
    image: new CircleStyle({
      radius: 12,
      fill: new Fill({ color }),
      stroke: new Stroke({ color: '#333', width: 1 })
    }),
    text: new Text({
      text: String(size),
      fill: new Fill({ color: '#000' })
    })
  });
}

Такой подход создаёт интуитивную визуальную шкалу плотности данных.


Масштаб и поведение кластеров

Кластеризация тесно связана с уровнем масштабирования карты. При изменении масштаба происходит перерасчёт групп:

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

OpenLayers автоматически пересчитывает кластерные группы при изменении resolution.


Оптимизация производительности

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

Основные принципы производительности:

  • использование одного VectorSource как базового хранилища;
  • минимизация сложной логики внутри style функции;
  • отказ от пересоздания источников без необходимости;
  • использование простых геометрий (Point вместо сложных объектов).

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


Пользовательское взаимодействие с кластерами

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

Пример обработки клика:

map.on('click', function (evt) {
  map.forEachFeatureAtPixel(evt.pixel, function (feature) {
    const clusteredFeatures = feature.get('features');

    if (clusteredFeatures.length > 1) {
      const extent = createEmptyExtent();

      clusteredFeatures.forEach(f => {
        extendExtent(extent, f.getGeometry().getExtent());
      });

      map.getView().fit(extent, {
        duration: 500,
        maxZoom: 15
      });
    }
  });
});

Такой подход позволяет:

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

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

Параметр distance может быть динамическим:

clusterSource.setDistance(60);

Также возможна пересоздача источника при изменении условий:

const newClusterSource = new Cluster({
  distance: currentZoom < 5 ? 80 : 30,
  source: vectorSource
});

Это позволяет адаптировать кластеризацию под масштаб.


Разделение кластеризации по типам данных

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

  • один VectorSource на категорию;
  • отдельный Cluster для каждого слоя;
  • независимая стилизация групп.

Пример:

const hotelsCluster = new Cluster({
  distance: 50,
  source: hotelsSource
});

const restaurantsCluster = new Cluster({
  distance: 50,
  source: restaurantsSource
});

Такой подход сохраняет семантику данных и упрощает визуальную интерпретацию.


Ограничения кластеризации

Несмотря на эффективность, механизм имеет ограничения:

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

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