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

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

В MapLibre GL JS количество точек может вычисляться несколькими способами:

  • подсчёт объектов в исходном массиве данных;
  • подсчёт объектов внутри текущих границ карты;
  • подсчёт объектов определённого слоя;
  • подсчёт кластеров и объектов внутри кластеров;
  • динамический подсчёт после фильтрации данных.

Выбор подхода зависит от структуры приложения и объёма отображаемой информации.


Подготовка GeoJSON-источника

Предположим, имеется набор точек в формате GeoJSON.

const points = {
    type: "FeatureCollection",
    features: [
        {
            type: "Feature",
            geometry: {
                type: "Point",
                coordinates: [37.6176, 55.7558]
            },
            properties: {
                name: "Москва"
            }
        },
        {
            type: "Feature",
            geometry: {
                type: "Point",
                coordinates: [30.3141, 59.9386]
            },
            properties: {
                name: "Санкт-Петербург"
            }
        },
        {
            type: "Feature",
            geometry: {
                type: "Point",
                coordinates: [49.1221, 55.7887]
            },
            properties: {
                name: "Казань"
            }
        }
    ]
};

Добавление источника и слоя:

map.on("load", () => {

    map.addSource("cities", {
        type: "geojson",
        data: points
    });

    map.addLayer({
        id: "cities-layer",
        type: "circle",
        source: "cities",
        paint: {
            "circle-radius": 6,
            "circle-color": "#1976d2"
        }
    });

});

Получение общего количества точек

Если все точки уже находятся в объекте GeoJSON, самый простой способ — использовать массив features.

const count = points.features.length;

console.log(count);

Результат:

3

Такой вариант подходит для:

  • отображения общего количества объектов;
  • формирования отчётов;
  • первоначальной загрузки данных.

Вывод количества в HTML:

<div id="counter"></div>
document.getElementById("counter").textContent =
    `Количество точек: ${points.features.length}`;

Подсчёт после загрузки данных

Иногда данные загружаются с сервера.

fetch("/data/cities.geojson")
    .then(response => response.json())
    .then(data => {

        map.getSource("cities").setData(data);

        const count = data.features.length;

        document.getElementById("counter").textContent =
            `Количество точек: ${count}`;
    });

В этом случае количество вычисляется сразу после получения GeoJSON.


Получение данных из источника карты

Если необходимо получить данные непосредственно из источника MapLibre GL JS, используется метод getSource().

const source = map.getSource("cities");

Однако объект источника не предоставляет универсальный публичный метод для чтения исходного GeoJSON обратно из карты. Поэтому обычно данные сохраняются отдельно.

Пример:

let citiesData = points;

map.addSource("cities", {
    type: "geojson",
    data: citiesData
});

Позже:

const count = citiesData.features.length;

Подобная практика считается наиболее надёжной при работе с большими приложениями.


Подсчёт точек в текущей области просмотра

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

Для этого используются текущие границы карты.

Получение границ:

const bounds = map.getBounds();

Затем производится проверка каждой точки.

function countVisiblePoints(data) {

    const bounds = map.getBounds();

    return data.features.filter(feature => {

        const [lng, lat] = feature.geometry.coordinates;

        return bounds.contains([lng, lat]);

    }).length;
}

Использование:

const visibleCount = countVisiblePoints(points);

console.log(visibleCount);

Автоматическое обновление счётчика при перемещении карты

Количество объектов должно меняться при панорамировании и масштабировании.

function updateCounter() {

    const count = countVisiblePoints(points);

    document.getElementById("counter").textContent =
        `Видимых точек: ${count}`;
}

Подписка на события:

map.on("moveend", updateCounter);
map.on("zoomend", updateCounter);

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


Использование queryRenderedFeatures

MapLibre GL JS позволяет получать объекты, реально отрисованные на карте.

Для этого используется метод:

map.queryRenderedFeatures();

Подсчёт объектов слоя:

const features = map.queryRenderedFeatures({
    layers: ["cities-layer"]
});

const count = features.length;

Полный пример:

function updateRenderedCount() {

    const features = map.queryRenderedFeatures({
        layers: ["cities-layer"]
    });

    document.getElementById("counter").textContent =
        `Отображается объектов: ${features.length}`;
}

Преимущества метода:

  • учитываются текущие фильтры;
  • учитываются границы экрана;
  • учитывается фактическая отрисовка слоя.

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


Подсчёт точек после применения фильтра

Предположим, имеются категории объектов.

properties: {
    category: "capital"
}

Устанавливается фильтр:

map.setFilter("cities-layer", [
    "==",
    ["get", "category"],
    "capital"
]);

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

const features = map.queryRenderedFeatures({
    layers: ["cities-layer"]
});

const count = features.length;

Количество автоматически будет соответствовать текущему фильтру.


Подсчёт объектов через querySourceFeatures

Метод querySourceFeatures() работает напрямую с источником данных.

const features = map.querySourceFeatures("cities");

console.log(features.length);

Отличия от queryRenderedFeatures():

Метод Учитывает экран Учитывает стиль
queryRenderedFeatures Да Да
querySourceFeatures Нет Нет

Поэтому второй метод полезен для получения полного количества объектов независимо от того, отображаются они сейчас или нет.


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

Иногда счётчик удобнее разместить поверх карты.

HTML:

<div id="map"></div>

<div id="stats">
    Точек: 0
</div>

CSS:

#stats {
    position: absolute;
    top: 10px;
    left: 10px;
    background: white;
    padding: 10px;
    border-radius: 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,.2);
    z-index: 1;
}

Обновление данных:

function updateStats() {

    const count = points.features.length;

    document.getElementById("stats").textContent =
        `Точек: ${count}`;
}

Работа с кластеризацией

При большом количестве объектов часто применяется кластеризация.

Источник:

map.addSource("earthquakes", {
    type: "geojson",
    data: data,
    cluster: true,
    clusterRadius: 50
});

В этом случае отдельные точки объединяются в группы.

MapLibre автоматически создаёт специальное свойство:

point_count

Оно содержит количество объектов внутри кластера.

Пример слоя кластеров:

map.addLayer({
    id: "clusters",
    type: "circle",
    source: "earthquakes",
    filter: ["has", "point_count"]
});

Получение информации:

map.on("click", "clusters", e => {

    const cluster =
        map.queryRenderedFeatures(e.point, {
            layers: ["clusters"]
        })[0];

    console.log(cluster.properties.point_count);

});

Результат:

125

Значение означает, что кластер содержит 125 точек.


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

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

Слой подписей:

map.addLayer({
    id: "cluster-count",
    type: "symbol",
    source: "earthquakes",
    filter: ["has", "point_count"],
    layout: {
        "text-field": ["get", "point_count"],
        "text-size": 14
    }
});

Теперь каждый кластер будет отображать количество входящих в него объектов.


Получение количества объектов внутри выбранного кластера

Для более сложных сценариев используется метод getClusterLeaves().

const source = map.getSource("earthquakes");

Получение объектов:

source.getClusterLeaves(
    clusterId,
    Infinity,
    0,
    (error, features) => {

        if (error) return;

        console.log(features.length);

    }
);

Таким способом можно получить реальное число элементов кластера и сами объекты.


Динамический подсчёт после поиска

Допустим, пользователь выполняет поиск по названию города.

function searchCities(query) {

    const filtered = points.features.filter(feature =>
        feature.properties.name
            .toLowerCase()
            .includes(query.toLowerCase())
    );

    document.getElementById("counter").textContent =
        `Найдено: ${filtered.length}`;
}

Пример:

searchCities("ка");

Результат:

Найдено: 1

Подсчёт объектов нескольких слоёв

Иногда необходимо определить общее количество объектов из нескольких слоёв.

const features = map.queryRenderedFeatures({
    layers: [
        "cities-layer",
        "airports-layer",
        "stations-layer"
    ]
});

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

const totalCount = features.length;

Либо отдельно по каждому слою:

const cities = map.queryRenderedFeatures({
    layers: ["cities-layer"]
}).length;

const airports = map.queryRenderedFeatures({
    layers: ["airports-layer"]
}).length;

const stations = map.queryRenderedFeatures({
    layers: ["stations-layer"]
}).length;

Оптимизация при большом количестве данных

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

Основные рекомендации:

  • хранить исходный GeoJSON отдельно от карты;
  • избегать полного перебора массива при каждом движении карты;
  • использовать кластеризацию;
  • применять queryRenderedFeatures() только по необходимым слоям;
  • обновлять счётчики через moveend, а не через move;
  • использовать серверный подсчёт для очень больших наборов данных.

Пример неэффективного решения:

map.on("move", () => {
    countVisiblePoints(points);
});

Такой код может выполняться сотни раз в секунду.

Предпочтительный вариант:

map.on("moveend", () => {
    countVisiblePoints(points);
});

Синхронизация нескольких счётчиков

В крупных приложениях часто одновременно отображаются:

  • общее количество объектов;
  • количество объектов после фильтрации;
  • количество объектов в текущей области карты;
  • количество кластеров.

Пример:

function updateStatistics() {

    const total =
        points.features.length;

    const visible =
        countVisiblePoints(points);

    const rendered =
        map.queryRenderedFeatures({
            layers: ["cities-layer"]
        }).length;

    console.log({
        total,
        visible,
        rendered
    });
}

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