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

В приложениях, построенных на основе Deck.gl, карта редко существует изолированно. Её положение, масштаб, наклон и ориентация часто должны быть согласованы с другими компонентами интерфейса:

  • несколькими картами на одном экране;
  • таблицами данных;
  • панелями фильтрации;
  • графиками и диаграммами;
  • внешними библиотеками визуализации;
  • серверными источниками данных.

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

Основным объектом состояния выступает view state — набор параметров камеры, определяющий текущий вид карты.

Типичный объект состояния выглядит следующим образом:

const viewState = {
  longitude: 37.6176,
  latitude: 55.7558,
  zoom: 10,
  pitch: 45,
  bearing: 0
};

Каждое изменение положения карты приводит к обновлению этих значений.


Управляемый и неуправляемый режимы

Deck.gl поддерживает два подхода к управлению состоянием карты.

Неуправляемый режим

В этом случае библиотека самостоятельно изменяет положение камеры.

new Deck({
  initialViewState: {
    longitude: -122.4,
    latitude: 37.74,
    zoom: 11
  },
  controller: true,
  layers: []
});

Свойство initialViewState используется только во время инициализации.

После запуска приложения Deck.gl самостоятельно хранит текущее состояние.

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

  • минимальный объём кода;
  • простая настройка;
  • подходит для небольших проектов.

Недостатки:

  • невозможно легко синхронизировать карту с другими компонентами;
  • ограниченный контроль над навигацией.

Управляемый режим

В крупных приложениях обычно используется внешний источник состояния.

new Deck({
  viewState,
  controller: true,
  layers: []
});

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

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

Такой подход обеспечивает:

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

Обработка изменений положения карты

Для получения нового состояния используется обработчик onViewStateChange.

new Deck({
  viewState,
  controller: true,

  onViewStateChange: ({viewState}) => {
    updateViewState(viewState);
  }
});

Во время перемещения карты Deck.gl передаёт актуальные параметры камеры.

Структура аргумента:

{
  viewState,
  interactionState,
  oldViewState
}

Где:

  • viewState — новое состояние;
  • oldViewState — предыдущее состояние;
  • interactionState — информация о действиях пользователя.

Пример:

onViewStateChange: ({viewState, interactionState}) => {
  console.log(interactionState.isDragging);
  console.log(interactionState.isZooming);

  updateViewState(viewState);
}

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

Наиболее распространённый сценарий — хранение состояния в React.

import {useState} from "react";
import DeckGL from "@deck.gl/react";

function Map() {
  const [viewState, setViewState] = useState({
    longitude: -74,
    latitude: 40.7,
    zoom: 12
  });

  return (
    <DeckGL
      controller={true}
      viewState={viewState}
      onViewStateCha nge={({viewState}) =>
        setViewState(viewState)
      }
    />
  );
}

Схема работы:

  1. Пользователь перемещает карту.
  2. Deck.gl генерирует новое состояние.
  3. Вызывается onViewStateChange.
  4. React обновляет состояние.
  5. Компонент перерисовывается.
  6. Новое состояние передаётся обратно в Deck.gl.

Такой цикл делает React единственным источником правды.


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

При наличии сложной архитектуры состояние карты часто хранится в Redux.

Создание действия:

export const setViewState = payload => ({
  type: "SET_VIEW_STATE",
  payload
});

Редьюсер:

const initialState = {
  longitude: 0,
  latitude: 0,
  zoom: 2
};

export default function reducer(
  state = initialState,
  action
) {
  switch (action.type) {
    case "SET_VIEW_STATE":
      return {
        ...state,
        ...action.payload
      };

    default:
      return state;
  }
}

Подключение Deck.gl:

<DeckGL
  viewState={viewState}
  controller={true}
  onViewStateCha nge={({viewState}) =>
    dispatch(setViewState(viewState))
  }
/>

Преимущества централизованного хранения:

  • единое состояние приложения;
  • возможность отката изменений;
  • поддержка DevTools;
  • удобная отладка.

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

Часто требуется отображать несколько карт одновременно.

Например:

  • карта спутниковых снимков;
  • карта дорожной сети;
  • карта плотности населения.

Все представления должны перемещаться синхронно.

Общий объект состояния

const [viewState, setViewState] = useState({
  longitude: -122.4,
  latitude: 37.7,
  zoom: 10
});

Первая карта:

<DeckGL
  viewState={viewState}
  controller={true}
  onViewStateCha nge={({viewState}) =>
    setViewState(viewState)
  }
/>

Вторая карта:

<DeckGL
  viewState={viewState}
  controller={false}
/>

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


Двунаправленная синхронизация

Если обе карты должны реагировать на действия пользователя:

const sync = ({viewState}) => {
  setViewState(viewState);
};

<DeckGL
  viewState={viewState}
  controller={true}
  onViewStateCha nge={sync}
/>

<DeckGL
  viewState={viewState}
  controller={true}
  onViewStateCha nge={sync}
/>

Обе карты используют единый источник состояния.


Ограничение области навигации

Иногда необходимо запретить выход за определённые координаты.

Для этого состояние модифицируется перед сохранением.

onViewStateChange: ({viewState}) => {
  viewState.latitude = Math.min(
    85,
    Math.max(-85, viewState.latitude)
  );

  setViewState(viewState);
}

Можно ограничить:

  • широту;
  • долготу;
  • масштаб;
  • угол наклона;
  • ориентацию.

Пример ограничения масштаба:

onViewStateChange: ({viewState}) => {
  viewState.zoom = Math.min(
    15,
    Math.max(5, viewState.zoom)
  );

  setViewState(viewState);
}

Синхронизация с URL

Многие геоинформационные системы сохраняют положение карты в адресной строке.

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

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

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

onViewStateChange: ({viewState}) => {
  const params = new URLSearchParams({
    lng: viewState.longitude,
    lat: viewState.latitude,
    zoom: viewState.zoom
  });

  history.replaceState(
    null,
    "",
    `?${params}`
  );
}

Восстановление состояния:

const params = new URLSearchParams(
  window.location.search
);

const viewState = {
  longitude: Number(params.get("lng")),
  latitude: Number(params.get("lat")),
  zoom: Number(params.get("zoom"))
};

Подобный подход широко применяется в аналитических и картографических системах.


Синхронизация с сервером

Некоторые приложения сохраняют текущее положение карты на сервере.

Например:

  • рабочее место диспетчера;
  • корпоративная GIS-платформа;
  • облачная аналитическая система.

Отправка данных:

onViewStateChange: ({viewState}) => {
  saveViewState(viewState);
}

Запрос:

async function saveViewState(viewState) {
  await fetch("/api/viewstate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(viewState)
  });
}

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


Синхронизация с внешними элементами интерфейса

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

Ползунок масштаба

<input
  type="range"
  min="1"
  max="20"
  value={viewState.zoom}
  onCha nge={e =>
    setViewState({
      ...viewState,
      zoom: Number(e.target.value)
    })
  }
/>

Изменение ползунка приводит к изменению масштаба карты.


Поле поиска

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

setViewState({
  longitude: place.lng,
  latitude: place.lat,
  zoom: 15
});

Таблица объектов

При выборе строки:

function selectRow(item) {
  setViewState({
    longitude: item.longitude,
    latitude: item.latitude,
    zoom: 14
  });
}

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


Анимация переходов состояния

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

Deck.gl поддерживает плавные переходы.

import {
  FlyToInterpolator
} from "@deck.gl/core";

Создание нового состояния:

setViewState({
  longitude: -73.985,
  latitude: 40.748,
  zoom: 14,

  transitionInterpolator:
    new FlyToInterpolator(),

  transitionDuration: 3000
});

В результате камера плавно перелетает к новой точке.

Подобные переходы особенно полезны при:

  • поиске объектов;
  • переключении регионов;
  • демонстрации данных;
  • навигации между сценами.

Синхронизация между Deck.gl и Mapbox

Одним из наиболее популярных сценариев является использование Deck.gl поверх Mapbox.

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

Пример общего состояния:

const [viewState, setViewState] = useState({
  longitude: -122.4,
  latitude: 37.8,
  zoom: 11,
  pitch: 45,
  bearing: 0
});

Передача состояния в Mapbox:

<Map
  {...viewState}
  onM ove={event =>
    setViewState(event.viewState)
  }
/>

Передача состояния в Deck.gl:

<DeckGL
  viewState={viewState}
  controller={true}
  onViewStateCha nge={({viewState}) =>
    setViewState(viewState)
  }
/>

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


Предотвращение лишних обновлений

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

Неоптимизированная синхронизация приводит к:

  • лишним перерисовкам;
  • нагрузке на React;
  • повышенному потреблению памяти;
  • снижению FPS.

Часто применяется ограничение частоты обновлений.

Пример с использованием requestAnimationFrame:

let frame = null;

function update(viewState) {
  if (frame) return;

  frame = requestAnimationFrame(() => {
    setViewState(viewState);
    frame = null;
  });
}

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

onViewStateChange: ({viewState}) => {
  update(viewState);
}

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


Работа с interactionState

Объект interactionState позволяет определить текущее действие пользователя.

Пример:

onViewStateChange: ({
  viewState,
  interactionState
}) => {

  if (interactionState.isDragging) {
    console.log("Перемещение карты");
  }

  if (interactionState.isZooming) {
    console.log("Изменение масштаба");
  }

  setViewState(viewState);
}

Доступные признаки обычно включают:

interactionState.isDragging
interactionState.isZooming
interactionState.isRotating
interactionState.inTransition

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


Типичные архитектурные схемы

Локальное состояние компонента

Подходит для небольших приложений.

Deck.gl
   ↕
React State

Централизованный Store

Подходит для средних и крупных проектов.

Deck.gl
   ↕
Redux / Zustand
   ↕
Остальные компоненты

URL как источник состояния

Подходит для аналитических панелей.

Deck.gl
   ↕
URL
   ↕
История браузера

Серверное состояние

Используется в многопользовательских системах.

Deck.gl
   ↕
Store
   ↕
Backend API
   ↕
База данных

Практические рекомендации

Управляемый режим (viewState) предпочтителен для большинства профессиональных приложений.

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

Сохранение состояния в URL повышает удобство навигации и обмена ссылками.

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

Синхронизация через глобальный Store облегчает поддержку сложных геоинформационных систем.

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