При отображении большого количества точечных объектов на карте возникает проблема перегруженности визуализации. При увеличении масштаба десятки и сотни маркеров начинают перекрывать друг друга, что снижает читаемость и затрудняет взаимодействие с данными.
Кластеризация решает эту задачу за счёт объединения близко расположенных точек в один объект, который отображает агрегированную информацию. В зависимости от масштаба карты такие группы динамически изменяются: при отдалении точки объединяются, при приближении — разделяются.
Ключевые задачи кластеризации:
В 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
});
Этот источник содержит «сырые» точки без группировки.
Кластеризация создаётся поверх существующего источника:
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 функции;Дополнительно можно отключать анимации перерисовки при массовом обновлении данных.
Кластеры часто используются как интерактивные элементы. При клике можно раскрывать группу или масштабировать карту.
Пример обработки клика:
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
});
Такой подход сохраняет семантику данных и упрощает визуальную интерпретацию.
Несмотря на эффективность, механизм имеет ограничения:
Для задач с высокой точностью отображения может использоваться комбинированный подход: кластеризация на низких масштабах и индивидуальные маркеры на высоких.