Кластеризация в веб-картографии используется для объединения множества близко расположенных точечных объектов в один агрегированный элемент. Это снижает визуальную перегрузку карты и улучшает производительность при работе с большими наборами данных.
В OpenLayers кластеризация реализуется через специальный источник
данных ol/source/Cluster, который оборачивает другой
векторный источник и динамически группирует объекты в зависимости от
масштаба отображения.
Кластеризация основана на вычислении расстояния между объектами в пикселях экрана. Если точки оказываются ближе заданного порога, они объединяются в один кластер.
Ключевые параметры:
Внутренне каждый кластер становится отдельным 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 формируют разные визуальные
сценарии:
Выбор значения зависит от плотности исходных данных и целей визуализации.