Синхронизация viewport

В Kepler.gl состояние отображения карты управляется через объект viewport, который описывает текущую камеру: координаты центра, масштаб, наклон, азимут и другие параметры визуализации. Этот объект является центральным связующим звеном между пользовательскими действиями, состоянием приложения и рендерингом WebGL-карты.

Viewport в Kepler.gl не является статической структурой. Он постоянно обновляется в ответ на взаимодействия пользователя (панорамирование, зум, вращение) и внешние изменения состояния (программное управление, синхронизация с Redux, переключение датасетов или слоёв).


Структура viewport

Базовый viewport в Kepler.gl обычно включает следующие поля:

  • latitude / longitude — координаты центра карты
  • zoom — уровень масштабирования
  • bearing — угол поворота карты (в градусах)
  • pitch — наклон камеры
  • width / height — размеры контейнера карты
  • altitude — параметр перспективы (используется Deck.gl)

Каждое из этих значений влияет на матрицу трансформации, которая передаётся в WebGL-рендерер через Deck.gl.


Механизм обновления viewport

Kepler.gl использует модель однонаправленного потока данных через Redux. Любое изменение viewport происходит через action:

  • пользователь взаимодействует с картой
  • Deck.gl генерирует событие изменения камеры
  • Kepler.gl dispatch’ит action обновления состояния
  • reducer записывает новый viewport в store
  • компоненты получают обновлённый state и перерисовываются

Ключевой action:

updateMapViewport({
  latitude,
  longitude,
  zoom,
  bearing,
  pitch,
  width,
  height
});

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


Проблема синхронизации состояния

При работе с Kepler.gl часто возникает необходимость синхронизировать viewport между несколькими источниками:

  • несколько экземпляров карты
  • внешние UI-контролы (слайдеры, инпуты)
  • URL-параметры
  • серверное состояние
  • сторонние библиотеки (например, Mapbox GL напрямую)

Основная сложность заключается в том, что viewport обновляется часто и непрерывно, особенно при перетаскивании карты. Это создаёт риск:

  • лишних ререндеров React
  • конфликтов состояний
  • «дёрганья» карты из-за циклических обновлений

Односторонняя и двусторонняя синхронизация

Односторонняя синхронизация

Наиболее стабильный подход — считать viewport производным состоянием:

UI → action → store → map

В этом случае любое внешнее изменение viewport проходит через Redux, а карта лишь отображает текущее состояние.

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

  • предсказуемость состояния
  • отсутствие циклов обновления
  • простая отладка

Недостаток:

  • невозможность мгновенного «внешнего захвата» состояния карты без dispatch

Двусторонняя синхронизация

Более сложная схема:

map ↔ store ↔ UI

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


Защита от циклических обновлений

Ключевая проблема при синхронизации viewport — повторный dispatch одного и того же состояния.

Типичный сценарий:

  1. пользователь двигает карту
  2. map вызывает updateMapViewport
  3. store обновляется
  4. React передаёт viewport обратно в карту
  5. карта снова интерпретирует изменение как новое событие

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

  • глубокое сравнение viewport
  • или частичное сравнение ключевых полей
  • или timestamp-based дедупликация

Пример защитной логики:

const isViewportEqual = (v1, v2) =>
  v1.latitude === v2.latitude &&
  v1.longitude === v2.longitude &&
  v1.zoom === v2.zoom &&
  v1.bearing === v2.bearing &&
  v1.pitch === v2.pitch;

Throttling и performance optimization

Viewport может обновляться десятки раз в секунду. Без оптимизации это приводит к перегрузке Redux и React.

Используются техники:

Throttle

Ограничение частоты dispatch:

import throttle from 'lodash.throttle';

const handleViewportChange = throttle((viewport) => {
  dispatch(updateMapViewport(viewport));
}, 50);

RequestAnimationFrame batching

Обновления синхронизируются с кадром рендеринга:

let pending = null;

function scheduleUpdate(vp) {
  pending = vp;
  requestAnimationFrame(() => {
    dispatch(updateMapViewport(pending));
  });
}

Внешняя синхронизация viewport

Viewport часто требуется синхронизировать с URL, чтобы обеспечить:

  • сохранение состояния карты
  • возможность шаринга ссылки
  • восстановление сессии

Пример сериализации:

?lat=53.9&lng=27.56&zoom=10&bearing=0&pitch=0

При изменении viewport:

  • происходит обновление query string
  • но важно избегать обратного цикла (URL → map → URL)

Решение:

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

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

Kepler.gl может использоваться в сценариях, где отображается несколько карт одновременно. В этом случае viewport может быть:

  • независимым
  • частично синхронизированным
  • полностью зеркальным

Жёсткая синхронизация

Все карты получают одинаковый viewport:

store.subscribe(() => {
  const vp = store.getState().map.viewport;
  mapA.setViewport(vp);
  mapB.setViewport(vp);
});

Условная синхронизация

Синхронизируются только:

  • zoom
  • center

Но не синхронизируются:

  • bearing
  • pitch

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


Взаимодействие viewport и layers

Viewport напрямую влияет на:

  • уровень детализации данных
  • кластеризацию
  • отрисовку больших наборов точек
  • LOD (Level of Detail) в Deck.gl

При изменении zoom может происходить:

  • переключение aggregation layers
  • пересчёт hexbin
  • динамическая фильтрация данных

Таким образом, viewport — это не только камера, но и триггер вычислений.


Интеграция с Deck.gl camera

Kepler.gl построен поверх Deck.gl, где viewport транслируется в параметры камеры:

  • viewState
  • controller state
  • projection matrix

Deck.gl использует viewport для расчёта:

  • screen coordinates
  • world projection
  • interaction picking

Любое изменение viewport фактически приводит к пересборке матрицы камеры.


Глубокая синхронизация с Redux middleware

Для сложных приложений viewport часто перехватывается middleware:

  • логирование перемещений камеры
  • аналитика пользовательского поведения
  • сохранение истории навигации

Пример middleware:

const viewportLogger = store => next => action => {
  if (action.type === 'UPDATE_MAP_VIEWPORT') {
    console.log('Viewport changed', action.payload);
  }
  return next(action);
};

Асинхронные источники viewport

Viewport может обновляться не только от пользователя:

  • геолокация
  • воспроизведение анимаций
  • replay движений
  • серверные сценарии

В таких случаях важно:

  • приоритизировать источник
  • предотвращать конфликт управления
  • использовать state machine для режимов камеры

Конфликт управления камерой

При одновременном управлении:

  • пользователь перетаскивает карту
  • система пытается анимировать viewport

возникает конфликт control authority.

Решение:

  • введение режима lock
  • приоритет user interaction
  • временное подавление программных обновлений

Итоговая модель поведения viewport

В зрелых приложениях viewport рассматривается как:

  • высокочастотное состояние
  • источник побочных эффектов
  • центральный объект синхронизации UI

Его корректная синхронизация требует сочетания:

  • Redux как единого источника истины
  • throttling для производительности
  • дедупликации для защиты от циклов
  • middleware для контроля потоков
  • строгого разделения источников обновления

Viewport в Kepler.gl становится не просто параметром камеры, а ядром всей интерактивной геовизуализации.