Drag and drop

Механизм drag and drop в Deck.gl основан на событийной модели уровня DeckGL и системе picking, которая связывает экранные координаты с объектами данных внутри слоёв. Перетаскивание объектов реализуется не как отдельная встроенная сущность слоя, а как комбинация обработчиков событий, обновления состояния и переинтерпретации данных слоя.

Ключевая идея заключается в том, что интерактивность в Deck.gl не изменяет геометрию напрямую — изменяются данные, на основе которых слой заново рендерится.


Система событий взаимодействия

Deck.gl предоставляет набор событий, которые формируют полный цикл drag and drop:

  • onDragStart
  • onDrag
  • onDragEnd
  • onHover
  • onClick

Эти события приходят из контроллера DeckGL и обрабатываются на уровне сцены. Каждое событие содержит:

  • x, y — координаты курсора в пикселях
  • coordinate — географические координаты (если используется карта)
  • object — объект данных, попавший под курсор (через picking)
  • layer — слой, из которого был получен объект
  • picked — флаг попадания в объект

События drag формируют непрерывный поток обновлений, где состояние объекта пересчитывается на каждом кадре перемещения мыши.


Picking как основа перетаскивания

Механизм picking выполняет преобразование:

экранные координаты → объект слоя → данные

Каждый слой может быть “pickable”, если установлен параметр:

pickable: true

При этом Deck.gl автоматически строит picking buffer и сопоставляет пиксели с объектами данных.

В контексте drag and drop важны следующие свойства результата picking:

  • index — индекс элемента в массиве данных слоя
  • object — исходный объект данных
  • coordinate — рассчитанная позиция на карте
  • sourceLayer — слой-источник

Жизненный цикл перетаскивания объекта

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

  1. onDragStart

    • фиксируется выбранный объект
    • сохраняется начальное состояние
    • определяется индекс элемента
  2. onDrag

    • вычисляется новая позиция курсора
    • преобразуется в координаты слоя
    • обновляется состояние данных
  3. onDragEnd

    • финализируется позиция
    • очищаются временные состояния

Важно, что Deck.gl не хранит состояние перетаскивания внутри слоя — оно реализуется внешним состоянием приложения.


Реализация базового drag для ScatterplotLayer

Для слоёв типа ScatterplotLayer типичный сценарий включает обновление массива данных:

const [data, setData] = useState(initialData);
const [draggedIndex, setDraggedIndex] = useState(null);
const [isDragging, setIsDragging] = useState(false);

Инициация слоя:

new ScatterplotLayer({
  id: 'points',
  data,
  pickable: true,
  getPosition: d => d.coordinates,
  getRadius: 100,
  onDragStart: info => {
    if (info.object) {
      setDraggedIndex(info.index);
      setIsDragging(true);
    }
  },
  onDrag: info => {
    if (!isDragging || draggedIndex === null) return;

    const newData = [...data];
    newData[draggedIndex] = {
      ...newData[draggedIndex],
      coordinates: info.coordinate
    };

    setData(newData);
  },
  onDragEnd: () => {
    setIsDragging(false);
    setDraggedIndex(null);
  }
});

Ключевой принцип: изменение данных вызывает перерасчёт геометрии слоя, что визуально воспринимается как перемещение объекта.


Преобразование координат

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

  • географические (lng, lat)
  • экранные (x, y)
  • локальные координаты WebGL

Для картографических слоёв Deck.gl автоматически выполняет преобразование через контекст view state.

Однако при кастомной логике часто требуется явное использование:

  • info.coordinate — итоговая географическая позиция
  • deck.project / deck.unproject — ручные преобразования

Особенно важно учитывать, что drag происходит в экранном пространстве, а обновление данных — в географическом.


Drag в контексте MapView и Mapbox

При использовании Deck.gl поверх Mapbox взаимодействие происходит через синхронизацию состояния вида:

<DeckGL
  initialViewState={viewState}
  controller={true}
  layers={layers}
/>

События drag влияют не только на объект, но и могут изменять:

  • центр карты (longitude, latitude)
  • zoom
  • bearing

Если drag используется для перемещения объектов, необходимо отключать влияние на камеру:

controller={{ dragPan: false }}

Разделение drag камеры и drag объектов

В Deck.gl существует два независимых типа drag:

  1. Drag камеры

    • перемещение карты
    • управляется Controller
  2. Drag объектов

    • изменение данных слоя
    • реализуется через onDrag*

Конфликт между ними возникает, когда оба режима активны одновременно. Для корректной работы вводится приоритет:

  • при попадании в объект камера блокируется
  • при пустом picking активируется pan

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

Сложные сцены требуют централизованного состояния:

  • draggedObject
  • sourceLayerId
  • initialCoordinate
  • offset

Пример вычисления смещения:

const offset = {
  lng: current.coordinate[0] - start.coordinate[0],
  lat: current.coordinate[1] - start.coordinate[1]
};

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


Drag для IconLayer и Object anchoring

В IconLayer перетаскивание часто связано с якорными точками:

new IconLayer({
  id: 'icons',
  data,
  pickable: true,
  getPosition: d => d.position,
  getIcon: d => d.icon,
  sizeScale: 1,
  onDrag: info => {
    if (!info.object) return;

    setData(prev =>
      prev.map((d, i) =>
        i === info.index
          ? { ...d, position: info.coordinate }
          : d
      )
    );
  }
});

Здесь важно учитывать anchor:

  • icon-anchor
  • getSize
  • масштабирование относительно zoom

Производительность drag операций

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

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

При больших наборах данных предпочтительно:

  • хранить координаты в TypedArray
  • обновлять только изменённый индекс
  • использовать updateTriggers

Использование updateTriggers в drag сценариях

new ScatterplotLayer({
  id: 'points',
  data,
  pickable: true,
  getPosition: d => d.coordinates,
  updateTriggers: {
    getPosition: draggedIndex
  }
});

Это позволяет ограничить перерасчёт только затронутыми элементами.


Кастомная реализация drag через layer extensions

Для сложных сценариев используется расширение слоя:

  • переопределение state
  • управление project внутри Layer
  • хранение временного drag состояния внутри layer.state

Подход применяется при:

  • групповых перемещениях объектов
  • привязке к сетке (snap-to-grid)
  • ограничениях области перемещения

Ограничения области перемещения

Часто требуется ограничить drag:

const clampLngLat = ([lng, lat]) => [
  Math.max(-180, Math.min(180, lng)),
  Math.max(-85, Math.min(85, lat))
];

Или в пределах bounding box:

if (lng < minLng || lng > maxLng) return;

Такая логика внедряется в обработчик onDrag.


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

При плавных перемещениях часто применяется интерполяция:

  • линейная
  • easing-функции
  • физическая модель (inertia)

Deck.gl не навязывает модель движения, поэтому обновление состояния может сопровождаться requestAnimationFrame для сглаживания траектории.


Многослойный drag и приоритеты picking

При наложении слоёв важен порядок:

  • верхний слой получает приоритет picking
  • getCursor может сигнализировать активный drag
  • visible влияет на участие в picking

Для сложных сцен используется фильтрация:

pickable: d => d.interactive

Согласование drag между слоями

При необходимости синхронного перемещения объектов разных слоёв применяется shared state:

  • единый store координат
  • событие broadcast drag
  • реактивное обновление всех слоёв

Такой подход используется при:

  • связях “точка–линия”
  • географических сетках
  • графовых структурах на карте