Кластеризация является одним из ключевых механизмов визуализации большого количества географических объектов на карте. При работе с тысячами и десятками тысяч точек отображение каждого объекта отдельно приводит к перегрузке интерфейса и снижению производительности. MapLibre GL JS предоставляет встроенную поддержку кластеризации GeoJSON-источников, позволяя объединять близко расположенные точки в группы.
Однако отображение самого кластера является лишь частью функциональности. Во многих сценариях требуется получить объекты, входящие в кластер, определить дочерние кластеры или извлечь все точки внутри конкретной группы. Для этого API MapLibre GL JS предоставляет специальные методы работы с кластеризованными источниками.
При включении кластеризации в GeoJSON-источнике:
map.addSource('earthquakes', {
type: 'geojson',
data: 'earthquakes.geojson',
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14
});
MapLibre автоматически создаёт специальные объекты-кластеры.
Каждый кластер содержит набор служебных свойств:
{
"cluster": true,
"cluster_id": 142,
"point_count": 385,
"point_count_abbreviated": "385"
}
Наиболее важным является поле:
cluster_id
Именно этот идентификатор используется для получения информации о содержимом кластера.
Чаще всего работа начинается после клика по кластеру.
Предположим, имеется слой кластеров:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count']
});
Получение данных выбранного кластера:
map.on('click', 'clusters', (e) => {
const feature = e.features[0];
console.log(feature);
});
Объект содержит свойства:
feature.properties.cluster
feature.properties.cluster_id
feature.properties.point_count
Например:
map.on('click', 'clusters', (e) => {
const clusterId =
e.features[0].properties.cluster_id;
console.log(clusterId);
});
Полученный cluster_id используется для дальнейших
запросов к источнику данных.
Для работы с кластером необходимо получить ссылку на источник:
const source =
map.getSource('earthquakes');
Тип объекта:
GeoJSONSource
Именно этот объект содержит методы работы с кластерами.
Метод:
source.getClusterChildren()
Возвращает непосредственных потомков кластера.
Синтаксис:
source.getClusterChildren(
clusterId,
callback
);
Пример:
map.on('click', 'clusters', (e) => {
const clusterId =
e.features[0].properties.cluster_id;
const source =
map.getSource('earthquakes');
source.getClusterChildren(
clusterId,
(error, features) => {
if (error) {
console.error(error);
return;
}
console.log(features);
}
);
});
Результатом будет массив объектов GeoJSON.
Например:
[
{
properties: {
cluster: true,
cluster_id: 200
}
},
{
properties: {
cluster: true,
cluster_id: 201
}
},
{
properties: {
magnitude: 4.3
}
}
]
Среди потомков могут присутствовать:
Поэтому необходимо дополнительно проверять наличие свойства:
feature.properties.cluster
Проверка выполняется следующим образом:
features.forEach(feature => {
if (feature.properties.cluster) {
console.log('Кластер');
} else {
console.log('Точка');
}
});
Такой подход позволяет строить собственную иерархию кластеров.
Метод:
source.getClusterLeaves()
Предназначен для получения конечных объектов внутри кластера.
В отличие от getClusterChildren(), который возвращает
только непосредственных потомков, данный метод извлекает реальные
точки.
Синтаксис:
source.getClusterLeaves(
clusterId,
limit,
offset,
callback
);
Параметры:
| Параметр | Описание |
|---|---|
| clusterId | идентификатор кластера |
| limit | количество возвращаемых объектов |
| offset | смещение |
| callback | функция получения результата |
Пример:
source.getClusterLeaves(
clusterId,
10,
0,
(error, features) => {
if (error) {
return;
}
console.log(features);
}
);
Будут возвращены первые десять точек кластера.
Часто требуется извлечь всё содержимое.
Для этого можно использовать количество объектов:
const count =
feature.properties.point_count;
Затем передать его как лимит:
source.getClusterLeaves(
clusterId,
count,
0,
(error, features) => {
console.log(features);
}
);
В результате будут получены все точки, входящие в кластер.
Каждый элемент массива представляет исходную запись GeoJSON.
Например:
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [
37.62,
55.75
]
},
properties: {
id: 15,
title: "Москва"
}
}
Это позволяет получить доступ ко всем пользовательским данным:
features.forEach(feature => {
console.log(feature.properties.title);
});
При большом количестве объектов не всегда целесообразно загружать их одновременно.
Пример получения данных страницами:
const pageSize = 100;
source.getClusterLeaves(
clusterId,
pageSize,
0,
callback
);
Следующая страница:
source.getClusterLeaves(
clusterId,
pageSize,
100,
callback
);
Ещё одна:
source.getClusterLeaves(
clusterId,
pageSize,
200,
callback
);
Подобный механизм удобен для:
Хотя этот метод не возвращает точки напрямую, он часто используется совместно с получением содержимого кластера.
Метод:
source.getClusterExpansionZoom()
Позволяет определить масштаб, на котором кластер будет разбит на более мелкие группы.
Пример:
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
console.log(zoom);
}
);
После получения масштаба карта может плавно приблизиться:
source.getClusterExpansionZoom(
clusterId,
(error, zoom) => {
if (error) {
return;
}
map.easeTo({
center:
e.features[0].geometry.coordinates,
zoom
});
}
);
Такой подход считается стандартным поведением кластеров в интерактивных картографических приложениях.
Полный пример:
map.on('click', 'clusters', (e) => {
const feature = e.features[0];
const clusterId =
feature.properties.cluster_id;
const count =
feature.properties.point_count;
const source =
map.getSource('earthquakes');
source.getClusterLeaves(
clusterId,
count,
0,
(error, leaves) => {
if (error) {
console.error(error);
return;
}
console.log(
'Количество точек:',
leaves.length
);
leaves.forEach(point => {
console.log(
point.properties
);
});
}
);
});
В результате после выбора кластера становится доступен полный список всех объектов, из которых он сформирован.
После получения точек можно выполнять дополнительную обработку.
Фильтрация:
const cities =
leaves.filter(item =>
item.properties.type === 'city'
);
Поиск:
const result =
leaves.find(item =>
item.properties.id === 125
);
Сортировка:
leaves.sort((a, b) =>
a.properties.population -
b.properties.population
);
Группировка:
const groups = {};
leaves.forEach(item => {
const type =
item.properties.type;
groups[type] ??= [];
groups[type].push(item);
});
Таким образом кластер может использоваться не только как элемент визуализации, но и как источник данных для аналитических операций.
Иногда требуется получить структуру кластеров целиком.
Для этого используется рекурсивный обход через
getClusterChildren().
Пример:
function traverseCluster(
source,
clusterId
) {
source.getClusterChildren(
clusterId,
(error, children) => {
if (error) {
return;
}
children.forEach(child => {
if (
child.properties.cluster
) {
traverseCluster(
source,
child.properties.cluster_id
);
} else {
console.log(
child.properties
);
}
});
}
);
}
Такой механизм позволяет:
После клика по кластеру:
source.getClusterLeaves(...)
Полученные точки отображаются в интерфейсе приложения.
Извлечённые объекты могут быть сохранены:
const json =
JSON.stringify(leaves);
или преобразованы в CSV.
После получения всех точек вычисляются агрегаты:
const average =
leaves.reduce(
(sum, item) =>
sum + item.properties.value,
0
) / leaves.length;
Кластер может рассматриваться как отдельная выборка данных:
{
clusterId,
count: leaves.length,
averageValue
}
Пользователь выбирает кластер и получает полный перечень входящих объектов без изменения масштаба карты.
| Метод | Назначение |
|---|---|
getClusterChildren() |
получение непосредственных потомков |
getClusterLeaves() |
получение конечных точек |
getClusterExpansionZoom() |
получение масштаба раскрытия |
cluster_id |
идентификатор конкретного кластера |
point_count |
количество точек внутри кластера |
Правильное использование этих методов позволяет эффективно работать с большими наборами геоданных, извлекать содержимое кластеров, строить пользовательскую навигацию по иерархии кластеризации и выполнять аналитическую обработку объектов непосредственно на стороне клиента.