Синхронизация данных

В архитектуре Mapbox GL JS данные рассматриваются как отдельный слой состояния, независимый от визуализации. Карта не хранит бизнес-логику приложения, а лишь отображает источники данных, которые могут изменяться динамически.

Базовые сущности работы с данными:

  • Source (источник) — контейнер данных (GeoJSON, Vector Tiles, Raster и др.)
  • Layer (слой) — визуальное представление данных
  • Style — связующее описание того, как источники отображаются

Синхронизация данных в этой модели означает поддержание актуальности Source и корректное инициирование перерисовки слоёв без полной реконструкции карты.


Типы источников и их поведение при обновлении

GeoJSONSource

Наиболее гибкий тип источника, предназначенный для динамических данных:

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

Обновление данных происходит через setData:

map.getSource('points').setData(newGeojson);

Особенность модели заключается в том, что каждый вызов setData инициирует:

  • переразбор GeoJSON
  • пересчёт геометрии
  • обновление всех связанных слоёв
  • планирование перерисовки кадра

VectorSource

Векторные тайлы обновляются иначе:

  • данные разбиты на тайлы
  • обновление происходит на уровне URL шаблонов
  • кеширование играет ключевую роль
map.addSource('tiles', {
  type: 'vector',
  tiles: ['https://example.com/{z}/{x}/{y}.pbf']
});

Синхронизация данных здесь не локальная, а серверная: изменения проявляются при смене тайлов или сбросе кеша.


Raster и Image источники

Для растровых данных обновление происходит через замену URL:

map.getSource('overlay').updateImage({
  url: 'https://example.com/image.png'
});

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


Механизм реактивности данных

Mapbox реализует обновление карты через внутренний цикл рендеринга, основанный на запросах перерисовки.

Каждое изменение источника вызывает:

  1. Пометку источника как «dirty»
  2. Инвалидацию зависимых слоёв
  3. Планирование render frame
  4. Пересборку буферов WebGL

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


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

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

При частых изменениях данных критично избегать многократных setData:

function updatePoints(batch) {
  const geojson = {
    type: 'FeatureCollection',
    features: batch
  };

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

Практика: агрегирование изменений перед передачей в Mapbox.


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

Для синхронизации с рендер-циклом:

let pending = null;

function scheduleUpdate(features) {
  pending = features;

  requestAnimationFrame(() => {
    if (!pending) return;

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

    pending = null;
  });
}

Синхронизация с внешними потоками данных

WebSocket поток

Типичный сценарий — потоковое обновление объектов:

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

socket.onmess age = (event) => {
  const data = JSON.parse(event.data);
  map.getSource('points').setData(data);
};

Основная проблема — частота сообщений. Без буферизации возможна деградация производительности из-за постоянного пересчёта геометрии.


Дельта-обновления

Вместо полной замены данных используются диффы:

function applyDelta(source, delta) {
  const current = source._data; // концептуально
  const merged = applyChanges(current, delta);

  source.setData(merged);
}

Хотя API Mapbox GL JS не предоставляет встроенного diff-режима для GeoJSON, паттерн часто реализуется на уровне приложения.


Управление состоянием данных

В сложных приложениях слой данных Mapbox синхронизируется с внешним state manager (Redux, Zustand и др.).

Типовая архитектура:

  • глобальное состояние хранит GeoJSON
  • изменения происходят через actions
  • карта подписывается на изменения
store.subscribe(() => {
  const state = store.getState();
  map.getSource('points').setData(state.geojson);
});

Ключевая проблема — предотвращение лишних перерисовок. Решается мемоизацией:

let lastHash = null;

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

  if (hash === lastHash) return;

  lastHash = hash;
  map.getSource('points').setData(state.geojson);
});

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

Кластеры GeoJSON чувствительны к обновлениям источника:

map.addSource('clusters', {
  type: 'geojson',
  data: geojson,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Каждый setData приводит к:

  • пересчёту кластеров
  • перераспределению точек
  • обновлению агрегированных свойств

При высоких объёмах данных рекомендуется:

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

Асинхронная загрузка и консистентность

Состояние источников может быть временно неконсистентным:

  • источник обновляется
  • слои уже пытаются отрендериться
  • WebGL буферы пересобираются

Для предотвращения «разрывов» используется стратегия:

map.once('idle', () => {
  map.getSource('points').setData(nextData);
});

Или последовательная очередь обновлений:

const queue = [];
let updating = false;

function enqueue(data) {
  queue.push(data);
  process();
}

function process() {
  if (updating || queue.length === 0) return;

  updating = true;

  const data = queue.shift();

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

  map.once('idle', () => {
    updating = false;
    process();
  });
}

Проблема частичных обновлений

GeoJSONSource не поддерживает частичное обновление feature. Любое изменение требует полной пересборки коллекции.

Следствие:

  • O(n) пересборка данных
  • увеличение GC pressure
  • рост времени кадра при больших наборах

Альтернативные подходы:

  • хранение данных по сегментам (несколько sources)
  • использование vector tiles
  • предагрегация на сервере

Синхронизация видимой области и данных

Иногда требуется подстраивать данные под viewport:

map.on('moveend', () => {
  const bounds = map.getBounds();

  fetch(`/data?bbox=${bounds.toArray().flat().join(',')}`)
    .then(res => res.json())
    .then(data => {
      map.getSource('points').setData(data);
    });
});

Особенность: данные синхронизируются с состоянием камеры, что создаёт динамическую модель «viewport-driven data».


Инвалидация и перерисовка

Внутренний механизм обновления карты строится вокруг концепции invalidation:

  • изменение source → invalidation tile cache
  • изменение layer → invalidation paint state
  • изменение style → full re-layout

Система стремится минимизировать full re-render, ограничиваясь частичной перерисовкой WebGL сцен.


Конкурентные обновления источников

При одновременных обновлениях:

  • несколько setData подряд
  • параллельные async fetch
  • конкурирующие WebSocket события

возникает проблема гонок состояния.

Решение:

let version = 0;

function update(data) {
  const current = ++version;

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

  requestAnimationFrame(() => {
    if (current !== version) return;
  });
}

Производственные паттерны синхронизации

Иммутабельный источник

Каждое обновление создаёт новый объект:

const next = {
  ...prev,
  features: [...prev.features, newFeature]
};

Сегментация источников

Разделение данных по логическим группам:

  • static-source
  • realtime-source
  • user-interaction-source

Debounce обновлений

const debounced = debounce((data) => {
  map.getSource('points').setData(data);
}, 100);

Синхронизация стилей и данных

Данные могут влиять на стиль отображения:

map.setPaintProperty('points-layer', 'circle-color', [
  'case',
  ['>', ['get', 'value'], 10],
  'red',
  'blue'
]);

Изменение данных автоматически пересчитывает визуальные выражения, что добавляет дополнительный слой синхронизации между data-driven style expressions и source updates.


Потоковая архитектура обновлений

При высоконагруженных сценариях используется pipeline:

  1. ingest (получение данных)
  2. transform (нормализация GeoJSON)
  3. diff/merge
  4. setData batch
  5. render cycle sync

Эта модель позволяет удерживать стабильный FPS при непрерывных потоках обновлений геоданных.