Кластеризация и её стилизация

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

В OpenLayers кластеризация реализуется через специальный источник данных ol/source/Cluster, который оборачивает другой векторный источник и динамически группирует объекты в зависимости от масштаба отображения.


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

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

Ключевые параметры:

  • distance — максимальное расстояние между объектами для объединения в кластер (в пикселях)
  • source — исходный векторный источник с точками
  • geometryFunction (опционально) — функция, определяющая геометрию кластеризации

Внутренне каждый кластер становится отдельным feature, содержащим массив исходных объектов.


Создание базовой кластеризации

Основная структура включает два слоя источников: исходный VectorSource и обёртку Cluster.

import VectorSource from 'ol/source/Vector';
import Cluster from 'ol/source/Cluster';
import GeoJSON from 'ol/format/GeoJSON';

const vectorSource = new VectorSource({
  features: new GeoJSON().readFeatures(geojsonData)
});

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

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


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

Кластеры отображаются через VectorLayer. Важно понимать: на слой передаётся именно clusterSource, а не исходный источник.

import VectorLayer from 'ol/layer/Vector';

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

Каждый кластер содержит свойство features, которое хранит массив объединённых объектов.


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

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

import Style from 'ol/style/Style';
import CircleStyle from 'ol/style/Circle';
import Fill from 'ol/style/Fill';
import Stroke from 'ol/style/Stroke';
import Text from 'ol/style/Text';

function createClusterStyle(size) {
  return new Style({
    image: new CircleStyle({
      radius: 12,
      fill: new Fill({ color: '#3399CC' }),
      stroke: new Stroke({
        color: '#fff',
        width: 2
      })
    }),
    text: new Text({
      text: size.toString(),
      fill: new Fill({
        color: '#fff'
      })
    })
  });
}

Дифференциация кластеров по размеру

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

function createClusterStyle(size) {
  let radius = 10;
  let color = '#3399CC';

  if (size > 50) {
    radius = 20;
    color = '#d73027';
  } else if (size > 20) {
    radius = 16;
    color = '#fc8d59';
  } else if (size > 5) {
    radius = 13;
    color = '#91bfdb';
  }

  return new Style({
    image: new CircleStyle({
      radius: radius,
      fill: new Fill({ color }),
      stroke: new Stroke({
        color: '#fff',
        width: 2
      })
    }),
    text: new Text({
      text: size.toString(),
      fill: new Fill({ color: '#fff' })
    })
  });
}

Зависимость кластеризации от масштаба

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

Для динамического управления можно пересоздавать источник:

map.getView().on('change:resolution', function () {
  const zoom = map.getView().getZoom();

  clusterSource.setDistance(
    zoom > 10 ? 20 : 60
  );
});

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

На высоких уровнях масштабирования кластеризация часто становится ненужной. Распространённая практика — отключать её при достижении определённого zoom.

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

const layer = new VectorLayer({
  source: clusterSource,
  style: function (feature) {
    const zoom = map.getView().getZoom();
    const features = feature.get('features');

    if (zoom > 15) {
      return new Style({
        image: new CircleStyle({
          radius: 6,
          fill: new Fill({ color: '#2b8cbe' })
        })
      });
    }

    return createClusterStyle(features.length);
  }
});

Обработка взаимодействия с кластерами

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

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

    if (clusteredFeatures.length > 1) {
      const extent = clusteredFeatures[0].getGeometry().getExtent();

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

Оптимизация кэширования стилей

Создание новых объектов Style при каждом рендере ухудшает производительность. Решение — кэширование стилей по размеру кластера.

const styleCache = {};

function getClusterStyle(size) {
  if (!styleCache[size]) {
    styleCache[size] = createClusterStyle(size);
  }
  return styleCache[size];
}

Геометрия кластера

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

const clusterSource = new Cluster({
  distance: 50,
  source: vectorSource,
  geometryFunction: function (feature) {
    return feature.getGeometry();
  }
});

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


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

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

const features = feature.get('features');

const totalValue = features.reduce((sum, f) => {
  return sum + f.get('value');
}, 0);

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


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

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

function animatedStyle(size, progress) {
  return new Style({
    image: new CircleStyle({
      radius: 5 + size * progress,
      fill: new Fill({
        color: `rgba(51, 153, 204, ${progress})`
      })
    })
  });
}

Кластеризация больших наборов данных

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

  • использование EPSG:3857 для вычислений
  • минимизация пересоздания источников
  • применение WebGLVectorLayer при необходимости
  • ограничение количества обновлений Cluster

Комбинирование кластеров и фильтрации

Кластеризация может сочетаться с фильтрацией по атрибутам:

const filteredSource = new VectorSource({
  features: allFeatures.filter(f => f.get('type') === 'city')
});

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

Многоуровневая кластеризация

В сложных системах применяется каскадная кластеризация:

  • первый уровень — грубое объединение (большой distance)
  • второй уровень — детализированное разбиение при увеличении масштаба

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


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

Различные значения distance формируют разные визуальные сценарии:

  • малый distance — высокая детализация, мало кластеров
  • средний distance — баланс плотности и читаемости
  • большой distance — сильная агрегация, упрощённая карта

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