Получение точек кластера

Кластеризация является одним из ключевых механизмов визуализации большого количества географических объектов на карте. При работе с тысячами и десятками тысяч точек отображение каждого объекта отдельно приводит к перегрузке интерфейса и снижению производительности. 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 количество точек внутри кластера

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