Синхронизация с картой

Синхронизация в контексте Mapbox GL JS означает согласование состояния карты с внешними источниками данных: пользовательским интерфейсом, URL, хранилищем приложения или другими компонентами системы. Ключевые параметры состояния карты включают центр, масштаб, наклон, азимут (bearing), а также активные слои, фильтры и выделенные объекты.

Mapbox GL JS предоставляет событийную модель и набор методов API, позволяющих отслеживать изменения состояния карты и программно их воспроизводить. Базовая единица синхронизации — объект карты, через который проходят все изменения.


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

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

Ключевые события:

  • move — любое изменение положения карты
  • moveend — завершение перемещения
  • zoom — изменение масштаба
  • zoomend — завершение масштабирования
  • rotate — изменение угла поворота
  • pitch — изменение наклона
  • render — обновление кадра карты

Пример базового отслеживания состояния:

map.on('moveend', () => {
  const center = map.getCenter();
  const zoom = map.getZoom();
  const bearing = map.getBearing();
  const pitch = map.getPitch();

  const state = {
    center: [center.lng, center.lat],
    zoom,
    bearing,
    pitch
  };

  console.log(state);
});

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


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

Двусторонняя синхронизация предполагает, что:

  1. карта обновляет внешнее состояние;
  2. внешнее состояние может обновлять карту.

Основная сложность заключается в предотвращении циклических обновлений.

Защита от рекурсивных обновлений

Типовой подход — использование флага блокировки:

let isSyncing = false;

map.on('moveend', () => {
  if (isSyncing) return;

  const center = map.getCenter();

  isSyncing = true;
  externalState.set({
    center: [center.lng, center.lat]
  });
  isSyncing = false;
});

При обратном обновлении:

externalState.subscribe((state) => {
  if (isSyncing) return;

  isSyncing = true;
  map.jumpTo({
    center: state.center,
    zoom: state.zoom
  });
  isSyncing = false;
});

Синхронизация центра, масштаба и ориентации

Наиболее частая задача — поддержание актуального состояния камеры карты.

Mapbox GL JS предоставляет два основных метода управления камерой:

  • jumpTo — мгновенное изменение
  • easeTo — плавная анимация

Мгновенная синхронизация

map.jumpTo({
  center: [lng, lat],
  zoom: 10,
  bearing: 0,
  pitch: 0
});

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

map.easeTo({
  center: [lng, lat],
  zoom: 10,
  duration: 800
});

Плавная синхронизация чаще используется при изменении состояния через интерфейс, так как снижает резкость визуальных переходов.


Синхронизация с URL (deep linking состояния карты)

URL может выступать источником истины для состояния карты, что позволяет сохранять и делиться текущим видом.

Кодирование состояния в hash

function updateHash() {
  const center = map.getCenter();
  const zoom = map.getZoom();
  const bearing = map.getBearing();
  const pitch = map.getPitch();

  const hash = `${zoom.toFixed(2)}/${center.lat.toFixed(5)}/${center.lng.toFixed(5)}/${bearing.toFixed(2)}/${pitch.toFixed(2)}`;
  window.location.hash = hash;
}

map.on('moveend', updateHash);

Восстановление состояния из URL

function parseHash() {
  const hash = window.location.hash.replace('#', '');
  const parts = hash.split('/');

  if (parts.length !== 5) return null;

  return {
    zoom: Number(parts[0]),
    lat: Number(parts[1]),
    lng: Number(parts[2]),
    bearing: Number(parts[3]),
    pitch: Number(parts[4])
  };
}

const state = parseHash();

if (state) {
  map.setCenter([state.lng, state.lat]);
  map.setZoom(state.zoom);
  map.setBearing(state.bearing);
  map.setPitch(state.pitch);
}

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

В архитектурах с состоянием (Redux, Zustand, Vuex) карта обычно рассматривается как view-layer компонент, синхронизируемый через store.

Подход через централизованное состояние

store.subscribe((state) => {
  const camera = state.map;

  map.jumpTo({
    center: camera.center,
    zoom: camera.zoom,
    bearing: camera.bearing,
    pitch: camera.pitch
  });
});

Обновление store:

map.on('moveend', () => {
  const center = map.getCenter();

  store.dispatch({
    type: 'MAP_UPDATE',
    payload: {
      center: [center.lng, center.lat],
      zoom: map.getZoom()
    }
  });
});

Синхронизация UI-компонентов с картой

Интерфейс часто включает элементы управления: слайдеры масштаба, поля координат, переключатели слоёв.

Синхронизация с zoom slider

map.on('zoom', () => {
  zoomSlider.value = map.getZoom();
});

zoomSlider.addEventListener('input', (e) => {
  map.setZoom(Number(e.target.value));
});

Синхронизация слоёв и фильтров

Состояние карты включает не только камеру, но и визуальные слои.

Управление видимостью слоя

function setLayerVisibility(layerId, visible) {
  map.setLayoutProperty(
    layerId,
    'visibility',
    visible ? 'visible' : 'none'
  );
}

Синхронизация фильтра данных

map.setFilter('points-layer', [
  '==',
  ['get', 'type'],
  currentType
]);

Обновление фильтра из состояния:

store.subscribe((state) => {
  map.setFilter('points-layer', [
    '==',
    ['get', 'type'],
    state.filterType
  ]);
});

Контроль производительности при синхронизации

Частые события move и zoom могут приводить к избыточным обновлениям внешнего состояния.

Дебаунсинг событий

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const syncState = debounce(() => {
  const center = map.getCenter();
  console.log(center);
}, 100);

map.on('move', syncState);

Использование moveend вместо move

Событие moveend уменьшает нагрузку, так как вызывается один раз после завершения интерактивного действия.


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

При работе с несколькими экземплярами карты возникает задача зеркалирования состояния.

Пример синхронизации двух карт

function syncMaps(source, target) {
  let syncing = false;

  source.on('move', () => {
    if (syncing) return;

    syncing = true;
    target.jumpTo({
      center: source.getCenter(),
      zoom: source.getZoom(),
      bearing: source.getBearing(),
      pitch: source.getPitch()
    });
    syncing = false;
  });
}

Такой подход применяется в сравнительных интерфейсах и режимах split-view.


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

Маркерные объекты могут быть частью синхронизируемого состояния.

const marker = new mapboxgl.Marker()
  .setLngLat([lng, lat])
  .addTo(map);

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

store.subscribe((state) => {
  marker.setLngLat(state.selectedPoint);
});

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

marker.getElement().addEventListener('click', () => {
  store.dispatch({
    type: 'SELECT_POINT',
    payload: marker.getLngLat()
  });
});

Синхронизация через queryRenderedFeatures

Интерактивные карты часто требуют синхронизации выбранных объектов.

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['points-layer']
  });

  if (features.length) {
    const feature = features[0];

    store.dispatch({
      type: 'SELECT_FEATURE',
      payload: feature.properties
    });
  }
});

Управление консистентностью состояния

При сложной синхронизации важно выделять единый источник истины. Распространённые архитектуры:

  • карта → store → UI
  • URL → store → карта
  • store → карта + UI

На практике предпочтительно избегать прямых связей «карта ↔︎ UI» без промежуточного слоя, чтобы исключить расхождение состояний.


Синхронизация анимаций и переходов

При использовании анимаций важно учитывать их асинхронность.

map.easeTo({
  center: [lng, lat],
  zoom: 12,
  duration: 1000
});

map.once('moveend', () => {
  console.log('Переход завершён');
});

Событие moveend используется как точка фиксации состояния после анимации.


Синхронизация в компонентных фреймворках

В React-подобных архитектурах карта обычно интегрируется через эффект жизненного цикла.

useEffect(() => {
  map.on('moveend', sync);

  return () => {
    map.off('moveend', sync);
  };
}, []);

Ключевая особенность — обязательная очистка подписок, предотвращающая утечки состояния.


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

При подключении realtime-источников (WebSocket, SSE) карта становится визуализационным слоем.

socket.on('update', (data) => {
  map.getSource('points').setData(data);
});

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

map.on('moveend', () => {
  socket.emit('viewport', {
    bounds: map.getBounds().toArray()
  });
});

Согласование состояния при загрузке стиля

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

map.on('styledata', () => {
  if (map.isStyleLoaded()) {
    restoreLayersFromState();
  }
});

Управление приоритетом источников состояния

При наличии нескольких источников (URL, store, UI) вводится приоритет:

  1. URL (начальное состояние)
  2. store (основное состояние приложения)
  3. UI (локальные изменения)

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


Стабилизация синхронизации при высокой частоте обновлений

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

  • интерполяция координат
  • ограничение частоты обновлений
  • игнорирование промежуточных состояний при активной анимации
let lastUpdate = 0;

map.on('move', () => {
  const now = Date.now();
  if (now - lastUpdate < 50) return;

  lastUpdate = now;
  externalSync(map.getCenter());
});