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

В MapLibre GL JS геоданные в формате GeoJSON подключаются через источник типа geojson. Такой источник работает как реактивная обёртка над набором пространственных объектов, которые визуализируются слоями карты. Обновление данных в этом источнике реализуется через полную замену содержимого, а не точечное изменение отдельных объектов, что определяет архитектуру динамических карт.

Создание GeoJSON-источника

Инициализация источника выполняется при загрузке карты. Источник становится частью внутреннего хранилища стиля и доступен по идентификатору.

map.on('load', () => {
  map.addSource('points-source', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: []
    }
  });

  map.addLayer({
    id: 'points-layer',
    type: 'circle',
    source: 'points-source',
    paint: {
      'circle-radius': 6,
      'circle-color': '#1978ff'
    }
  });
});

В этом контексте data представляет собой стандартный объект GeoJSON. MapLibre хранит его внутри Web Worker для обеспечения производительности при большом количестве объектов.


Базовый механизм обновления данных

Обновление GeoJSON осуществляется через метод setData, вызываемый у источника.

const source = map.getSource('points-source');

source.setData({
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [30.5, 50.5]
      },
      properties: {
        id: 1
      }
    }
  ]
});

Каждый вызов setData полностью заменяет предыдущее состояние источника. Внутренняя модель не выполняет дифференциальное обновление объектов, поэтому даже небольшое изменение требует передачи полного GeoJSON.


Особенности полной перезаписи данных

Модель обновления через замену имеет ряд последствий:

  • отсутствует частичное обновление feature по id
  • любые изменения требуют пересборки FeatureCollection
  • производительность зависит от размера передаваемого объекта
  • сериализация и передача данных в worker являются затратными операциями

При работе с большими наборами данных важно учитывать стоимость повторной отправки всей структуры.


Генерация обновлённого GeoJSON

Типичный подход заключается в хранении данных во внешнем состоянии приложения и генерации нового GeoJSON при каждом обновлении.

let features = [];

function updateFeaturePosition(id, newCoordinates) {
  features = features.map(f => {
    if (f.properties.id === id) {
      return {
        ...f,
        geometry: {
          type: 'Point',
          coordinates: newCoordinates
        }
      };
    }
    return f;
  });

  map.getSource('points-source').setData({
    type: 'FeatureCollection',
    features
  });
}

Такая схема отражает иммутабельный стиль обновления данных, при котором исходный массив не модифицируется напрямую.


Частые сценарии обновления данных

Потоковые обновления (WebSocket)

При поступлении данных в реальном времени каждое сообщение может изменять положение объектов или добавлять новые элементы.

socket.onmess age = (event) => {
  const update = JSON.parse(event.data);

  features = features.map(f => {
    if (f.properties.id === update.id) {
      return {
        ...f,
        geometry: {
          type: 'Point',
          coordinates: update.coordinates
        }
      };
    }
    return f;
  });

  map.getSource('points-source').setData({
    type: 'FeatureCollection',
    features
  });
};

При высокой частоте сообщений возникает необходимость агрегации обновлений.


Периодическое обновление (polling)

async function refreshData() {
  const response = await fetch('/api/points');
  const geojson = await response.json();

  map.getSource('points-source').setData(geojson);
}

setInterval(refreshData, 5000);

Этот подход прост, но создаёт регулярные пересоздания данных независимо от реальных изменений.


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

Батчинг обновлений

При поступлении множества событий за короткий интервал выполняется накопление изменений с последующим единичным вызовом setData.

let pending = false;

function scheduleUpdate() {
  if (pending) return;

  pending = true;

  requestAnimationFrame(() => {
    map.getSource('points-source').setData({
      type: 'FeatureCollection',
      features
    });

    pending = false;
  });
}

Такой подход синхронизирует обновления с циклом рендеринга браузера.


Минимизация размера GeoJSON

Снижение объёма данных уменьшает нагрузку на сериализацию:

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

Обновление отдельных типов данных

Точечные данные

Для Point наиболее частая операция — изменение координат. В MapLibre нет механизма частичного обновления, поэтому пересобирается вся коллекция.

Линии и полигоны

Для LineString и Polygon обновление требует полной замены массива координат:

feature.geometry.coordinates = newCoordinates;

map.getSource('points-source').setData({
  type: 'FeatureCollection',
  features
});

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


Кластеризация и обновление данных

При включённой кластеризации источник выполняет агрегацию точек в worker. Обновление через setData автоматически приводит к пересчёту кластеров.

map.addSource('clustered', {
  type: 'geojson',
  data: geojsonData,
  cluster: true,
  clusterRadius: 40
});

Любое изменение данных вызывает:

  • перерасчёт кластеров
  • обновление тайлов внутри текущего viewport
  • перерисовку слоёв кластеров

При больших наборах данных это становится наиболее затратной операцией.


Работа с большими объёмами данных

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

Используются следующие стратегии:

Разделение источников

Данные делятся по категориям или регионам:

  • статический слой (редко обновляемый)
  • динамический слой (часто обновляемый)

Географическая сегментация

Разделение по тайлам или регионам позволяет обновлять только часть данных.


Кэширование и переиспользование объектов

Частая ошибка — создание нового GeoJSON с полным копированием всех объектов при каждом обновлении. Это приводит к лишней нагрузке на GC и сериализацию.

Более эффективный подход заключается в переиспользовании структуры и изменении только необходимых частей:

function updateInPlace(id, coords) {
  const feature = featureMap.get(id);

  feature.geometry.coordinates = coords;

  map.getSource('points-source').setData({
    type: 'FeatureCollection',
    features: Array.from(featureMap.values())
  });
}

Хотя подход сохраняет ссылочную структуру, сам вызов setData остаётся полной заменой.


Синхронизация с состоянием приложения

В архитектурах на основе состояния (Redux, MobX, Zustand) GeoJSON часто является производным представлением.

store.subscribe(() => {
  const state = store.getState();

  const geojson = {
    type: 'FeatureCollection',
    features: state.points.map(p => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: p.coordinates
      },
      properties: p.meta
    }))
  };

  map.getSource('points-source').setData(geojson);
});

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


Типичные проблемы при обновлении GeoJSON

Потеря производительности при частых вызовах setData

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

Несоответствие структуры данных

GeoJSON должен строго соответствовать спецификации:

  • обязательный type
  • корректные Feature и FeatureCollection
  • валидные координаты

Ошибки в структуре могут приводить к молчаливому игнорированию обновления.

Пересоздание объектов без необходимости

Создание новых объектов вместо изменения существующих увеличивает нагрузку на сборщик мусора.


Архитектурный паттерн обновления источника

На практике используется единый слой абстракции:

  • внешнее состояние хранит данные
  • трансформер формирует GeoJSON
  • контроллер вызывает setData
function renderSource(state) {
  map.getSource('points-source').setData(toGeoJSON(state));
}

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