Динамическое обновление данных

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

В MapLibre GL JS динамическое обновление данных реализуется через изменение источников данных (sources), обновление геометрии объектов, изменение свойств объектов и обновление параметров визуализации слоёв без полной перезагрузки карты.

Основным механизмом является изменение содержимого источника GeoJSON через метод setData(), а также работа с состоянием объектов через feature-state.


Архитектура обновления данных

Отображение данных в MapLibre GL JS строится по следующей схеме:

Источник данных (Source)
        ↓
Слой (Layer)
        ↓
Визуализация на карте

Источник содержит географические данные:

map.addSource('vehicles', {
    type: 'geojson',
    data: {
        type: 'FeatureCollection',
        features: []
    }
});

Слой использует этот источник:

map.addLayer({
    id: 'vehicles-layer',
    type: 'circle',
    source: 'vehicles',
    paint: {
        'circle-radius': 8,
        'circle-color': '#ff0000'
    }
});

При обновлении данных источник остаётся тем же самым, а его содержимое заменяется.


Метод setData()

Главный инструмент динамического обновления GeoJSON-источников — метод setData().

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

const source = map.getSource('vehicles');

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

source.setData(newGeoJson);

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

const geojson = {
    type: 'FeatureCollection',
    features: [
        {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [37.6176, 55.7558]
            }
        }
    ]
};

map.getSource('vehicles').setData(geojson);

После вызова метода карта автоматически выполняет:

  1. Пересчёт тайлов.
  2. Обновление пространственных индексов.
  3. Перерисовку затронутых слоёв.
  4. Обновление подписей и символов.

Дополнительных действий не требуется.


Периодическое обновление данных

Наиболее распространённый сценарий — получение новых данных через фиксированные интервалы времени.

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

setInterval(async () => {
    const response = await fetch('/api/vehicles');
    const data = await response.json();

    map.getSource('vehicles').setData(data);

}, 5000);

Схема работы:

Сервер
   ↓
fetch()
   ↓
JSON
   ↓
setData()
   ↓
Обновление карты

Такой подход широко используется для:

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

Обновление после пользовательских действий

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

Пример поиска объектов:

async function search(query) {

    const response = await fetch(
        `/api/search?q=${encodeURIComponent(query)}`
    );

    const data = await response.json();

    map.getSource('search-results').setData(data);
}

Обработчик поля ввода:

input.addEventListener('input', event => {
    search(event.target.value);
});

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


Обновление отдельного объекта

Метод setData() всегда заменяет весь GeoJSON-документ.

Если требуется изменить один объект, обычно выполняются следующие действия:

const sourceData = map
    .getSource('vehicles')
    ._data;

Поиск объекта:

const vehicle = sourceData.features.find(
    item => item.properties.id === 25
);

Изменение координат:

vehicle.geometry.coordinates = [
    37.61,
    55.75
];

Обновление:

map
    .getSource('vehicles')
    .setData(sourceData);

Несмотря на изменение одного объекта, источник будет пересчитан целиком.


Анимация движения объектов

Часто необходимо отображать движение объекта по карте.

Простейший пример:

let lng = 37.6176;

setInterval(() => {

    lng += 0.001;

    const data = {
        type: 'FeatureCollection',
        features: [
            {
                type: 'Feature',
                geometry: {
                    type: 'Point',
                    coordinates: [lng, 55.7558]
                }
            }
        ]
    };

    map.getSource('vehicle').setData(data);

}, 100);

Объект будет плавно перемещаться вдоль оси долготы.


Анимация через requestAnimationFrame

Для более плавного движения используется механизм браузера requestAnimationFrame.

let position = 37.6176;

function animate() {

    position += 0.00005;

    map.getSource('vehicle').setData({
        type: 'FeatureCollection',
        features: [
            {
                type: 'Feature',
                geometry: {
                    type: 'Point',
                    coordinates: [
                        position,
                        55.7558
                    ]
                }
            }
        ]
    });

    requestAnimationFrame(animate);
}

animate();

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

  • синхронизация с частотой обновления экрана;
  • отсутствие лишних перерисовок;
  • более плавная анимация;
  • снижение нагрузки на процессор.

Работа с feature-state

Не всегда требуется обновлять геометрию объекта. Иногда необходимо изменить только его состояние.

Для таких задач используется механизм feature-state.

Источник:

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

Слой:

map.addLayer({
    id: 'cities-layer',
    type: 'circle',
    source: 'cities',
    paint: {
        'circle-color': [
            'case',
            ['boolean', ['feature-state', 'selected'], false],
            '#ff0000',
            '#0080ff'
        ]
    }
});

Изменение состояния:

map.setFeatureState(
    {
        source: 'cities',
        id: 15
    },
    {
        selected: true
    }
);

После этого цвет объекта изменится без обновления GeoJSON.


Удаление состояния объекта

Сброс состояния:

map.removeFeatureState({
    source: 'cities',
    id: 15
});

Удаление конкретного свойства:

map.removeFeatureState(
    {
        source: 'cities',
        id: 15
    },
    'selected'
);

Преимущества feature-state

Использование feature-state значительно эффективнее обновления всего источника.

Сравнение:

Операция Производительность
setData() Ниже
feature-state Выше

Причины:

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

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


Динамическое изменение свойств слоя

Обновляться могут не только данные, но и параметры отображения.

Изменение цвета:

map.setPaintProperty(
    'vehicles-layer',
    'circle-color',
    '#00ff00'
);

Изменение размера:

map.setPaintProperty(
    'vehicles-layer',
    'circle-radius',
    12
);

Изменение прозрачности:

map.setPaintProperty(
    'vehicles-layer',
    'circle-opacity',
    0.5
);

Все изменения отображаются немедленно.


Изменение параметров layout

Для свойств секции layout используется метод setLayoutProperty().

Скрытие слоя:

map.setLayoutProperty(
    'vehicles-layer',
    'visibility',
    'none'
);

Показ слоя:

map.setLayoutProperty(
    'vehicles-layer',
    'visibility',
    'visible'
);

Изменение текста подписи:

map.setLayoutProperty(
    'labels',
    'text-field',
    ['get', 'name']
);

Обновление данных через WebSocket

Для потоковых данных чаще используется WebSocket.

Подключение:

const socket = new WebSocket(
    'wss://example.com/ws'
);

Получение сообщений:

socket.onmess age = event => {

    const geojson =
        JSON.parse(event.data);

    map
        .getSource('vehicles')
        .setData(geojson);
};

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

  • минимальная задержка;
  • отсутствие постоянных HTTP-запросов;
  • эффективная работа с большим количеством обновлений.

Такой подход применяется для:

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

Инкрементальное обновление данных

При больших объёмах данных нежелательно постоянно передавать полный GeoJSON.

Вместо этого сервер может отправлять только изменения.

Пример сообщения:

{
  "id": 25,
  "lng": 37.62,
  "lat": 55.76
}

Клиент обновляет существующий объект:

socket.onmess age = event => {

    const update =
        JSON.parse(event.data);

    const feature =
        geojson.features.find(
            f => f.properties.id === update.id
        );

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

    map
        .getSource('vehicles')
        .setData(geojson);
};

Объём сетевого трафика существенно уменьшается.


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

Если используется кластеризация:

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

После вызова:

source.setData(newData);

кластеры пересчитываются автоматически.

Дополнительная настройка не требуется.


Обновление данных при перемещении карты

Иногда данные загружаются только для текущей области просмотра.

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

const bounds = map.getBounds();

Загрузка данных:

async function loadVisibleData() {

    const bounds =
        map.getBounds();

    const response =
        await fetch(
            `/api/data?bbox=${
                bounds.getWest()
            },${
                bounds.getSouth()
            },${
                bounds.getEast()
            },${
                bounds.getNorth()
            }`
        );

    const data =
        await response.json();

    map
        .getSource('objects')
        .setData(data);
}

Обновление после перемещения карты:

map.on('moveend', loadVisibleData);

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

  • снижение объёма данных;
  • ускорение отображения;
  • уменьшение потребления памяти.

Обработка событий загрузки данных

MapLibre предоставляет события, позволяющие отслеживать обновление источников.

Событие начала загрузки:

map.on('dataloading', event => {
    console.log(event);
});

Событие завершения:

map.on('data', event => {
    console.log(event);
});

Проверка полной загрузки:

if (map.isSourceLoaded('vehicles')) {
    console.log('Источник загружен');
}

Подобные механизмы полезны для отображения индикаторов загрузки.


Контроль частоты обновлений

Чрезмерно частые вызовы setData() могут снижать производительность.

Неудачный пример:

setInterval(updateData, 10);

Такой код создаёт сотни обновлений в секунду.

Более разумный вариант:

setInterval(updateData, 1000);

или

requestAnimationFrame(updateData);

Частота обновления должна соответствовать характеру данных:

Тип данных Частота
Погода 1–10 минут
Датчики 1–10 секунд
Транспорт 1–5 секунд
Анимация 60 FPS
Финансовые данные по событию

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

Наиболее важные практики повышения производительности:

Использование feature-state вместо setData(), когда изменяются только визуальные параметры объектов.

Загрузка данных по области видимости карты, а не всего набора данных.

Использование WebSocket вместо частого опроса сервера.

Минимизация размера GeoJSON, исключение ненужных свойств.

Снижение частоты обновлений, если данные не требуют мгновенного отображения.

Группировка изменений, когда несколько обновлений объединяются в одну операцию setData().

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

Отказ от полной пересборки объектов GeoJSON, если возможно обновление существующих структур данных.

Грамотно реализованное динамическое обновление позволяет MapLibre GL JS эффективно отображать десятки тысяч объектов и обрабатывать потоки данных в реальном времени без заметного снижения производительности интерфейса.