Динамическое обновление GeoJSON

Mapbox GL JS построена вокруг реактивной модели данных: карта не перерисовывается целиком при изменении информации, а пересчитывает только те слои, которые зависят от обновлённого источника данных. Центральным механизмом работы с динамическими пространственными данными выступает GeoJSON source, поддерживающий оперативное обновление через setData.

GeoJSON-источник в Mapbox GL JS представляет собой связующее звено между пользовательскими данными и рендерингом WebGL. Любое изменение объекта GeoJSON немедленно влияет на визуализацию всех слоёв, связанных с этим источником, при условии корректного вызова обновления.


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

Внутри Mapbox GL JS данные разделяются на два уровня:

  • Source (источник данных) — хранит GeoJSON
  • Layer (слой) — определяет способ визуализации

GeoJSON source может быть:

  • статическим (загружается один раз)
  • динамическим (обновляется в процессе работы приложения)

Динамическое поведение обеспечивается методом:

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

Каждый вызов setData инициирует:

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

Базовая схема динамического обновления

Структура источника создаётся один раз:

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

Слой, использующий данные:

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

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

function updateVehicles(newData) {
  const source = map.getSource('vehicles');
  source.setData(newData);
}

Обновление через потоковые данные

На практике GeoJSON редко обновляется вручную. Чаще используется поток данных:

  • WebSocket
  • Server-Sent Events
  • периодический polling

WebSocket-обновления

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

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

  const geojson = {
    type: 'FeatureCollection',
    features: data.map(item => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [item.lng, item.lat]
      },
      properties: {
        id: item.id,
        speed: item.speed
      }
    }))
  };

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

Ключевой момент — каждый пакет данных должен формировать полный GeoJSON, а не дельту.


Ограничения полного пересчёта данных

Метод setData заменяет весь источник целиком. Это означает:

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

При больших наборах данных (10k–100k объектов) это становится узким местом.


Стратегии оптимизации обновлений

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

Частые вызовы setData приводят к избыточным перерисовкам. Решение — агрегация данных:

let buffer = [];
let scheduled = false;

function pushUpdate(item) {
  buffer.push(item);

  if (!scheduled) {
    scheduled = true;

    requestAnimationFrame(() => {
      flushBuffer();
      scheduled = false;
    });
  }
}

function flushBuffer() {
  const source = map.getSource('vehicles');

  const geojson = {
    type: 'FeatureCollection',
    features: buffer.map(toFeature)
  };

  buffer = [];
  source.setData(geojson);
}

Инкрементальное обновление через локальное состояние

Вместо полного пересоздания GeoJSON можно хранить состояние отдельно:

const state = new Map();

function updateItem(item) {
  state.set(item.id, item);

  const features = Array.from(state.values()).map(toFeature);

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

Подход полезен при небольших и средних объёмах данных, но требует контроля памяти.


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

При росте числа объектов важны следующие приёмы:

1. Кластеризация

Mapbox GL JS поддерживает serverless clustering:

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

Кластеризация снижает нагрузку на GPU и уменьшает количество отрисовываемых объектов.


2. Фильтрация на уровне слоя

Слои позволяют ограничивать отображение:

map.setFilter('vehicles-layer', [
  '>',
  ['get', 'speed'],
  10
]);

Фильтрация уменьшает количество визуализируемых элементов без изменения источника.


3. Разделение источников по типам данных

Разделение GeoJSON на несколько источников уменьшает стоимость пересчёта:

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

Частичное обновление через feature-state

Хотя GeoJSON не поддерживает частичный update, Mapbox GL JS предоставляет механизм состояния объектов:

map.setFeatureState(
  { source: 'vehicles', id: 'vehicle-1' },
  { speed: 80 }
);

И слой может использовать выражения:

'circle-color': [
  'case',
  ['>', ['feature-state', 'speed'], 60],
  '#ff0000',
  '#00ff00'
]

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

  • обновляется только state
  • нет пересборки GeoJSON
  • минимальная нагрузка на render pipeline

Ограничение:

  • требуется уникальный id у features

Генерация GeoJSON в реальном времени

Формирование структуры данных критично для производительности:

function toFeature(item) {
  return {
    type: 'Feature',
    id: item.id,
    geometry: {
      type: 'Point',
      coordinates: [item.longitude, item.latitude]
    },
    properties: {
      speed: item.speed,
      heading: item.heading
    }
  };
}

Рекомендации:

  • использовать числовые id
  • избегать глубоких вложенных properties
  • минимизировать размер объекта

Обновление координат анимацией

Для плавного движения объектов используется интерполяция:

function animate(from, to, t) {
  return [
    from[0] + (to[0] - from[0]) * t,
    from[1] + (to[1] - from[1]) * t
  ];
}

И цикл обновления:

function tick() {
  const features = state.map(item => ({
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: animate(item.prev, item.next, item.progress)
    },
    properties: item.props
  }));

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

  requestAnimationFrame(tick);
}

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

При потоковых данных возможны ситуации:

  • устаревшие пакеты перекрывают новые
  • порядок сообщений нарушен
  • частичные обновления приходят позже полной выборки

Решение — версионирование:

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

  if (payload.version < currentVersion) return;

  currentVersion = payload.version;

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

Поведение при отсутствии источника

Если getSource вызывается до загрузки стиля:

map.on('load', () => {
  const source = map.getSource('vehicles');
});

Или проверка:

if (map.getSource('vehicles')) {
  map.getSource('vehicles').setData(data);
}

Важно учитывать асинхронность загрузки стиля карты.


Влияние обновлений на производительность рендера

Каждый вызов setData вызывает:

  • пересборку spatial index
  • обновление vertex buffers
  • пересчёт layout слоёв
  • repaint frame

Факторы нагрузки:

  • количество features
  • сложность геометрии
  • число активных слоёв
  • наличие фильтров и выражений

Оптимизация достигается не только снижением данных, но и уменьшением частоты обновлений.


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

В гибридных системах:

  • GeoJSON используется для динамики
  • vector tiles — для базового фона

Пример архитектуры:

  • static roads → vector tiles
  • moving objects → GeoJSON source
  • analytics overlays → feature-state

Это снижает нагрузку на сервер и клиент одновременно.


Устойчивые паттерны обновления

Debounce-обновления

let timeout;

function update(data) {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    map.getSource('layer').setData(data);
  }, 100);
}

RAF-координация

let pending = false;

function schedule(data) {
  if (pending) return;

  pending = true;

  requestAnimationFrame(() => {
    map.getSource('layer').setData(data);
    pending = false;
  });
}

Геометрические трансформации перед обновлением

Иногда данные требуют нормализации:

  • проекция координат
  • фильтрация некорректных точек
  • агрегация близких объектов
function sanitize(features) {
  return features.filter(f =>
    Array.isArray(f.geometry.coordinates)
  );
}

Сценарии реального времени

Типичные области применения:

  • трекинг транспорта
  • визуализация IoT-датчиков
  • отображение движения пользователей
  • мониторинг логистики
  • динамические heatmap-обновления

Во всех случаях ключевой механизм остаётся одинаковым: пересборка GeoJSON через setData и минимизация частоты вызовов.