Drag события

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

Общая модель drag-событий

Drag-события в Mapbox GL JS строятся вокруг жизненного цикла жеста перетаскивания:

  • начало взаимодействия (dragstart)
  • активное перемещение (drag)
  • завершение взаимодействия (dragend)

Эта модель применяется как к самой карте, так и к отдельным интерактивным объектам.

Основной принцип заключается в том, что любое drag-взаимодействие — это последовательность событий, отражающих изменения состояния pointer-устройства.

Drag карты: перемещение viewport

Карта в Mapbox GL JS по умолчанию поддерживает перетаскивание (pan). При движении мыши или пальца инициируются события:

  • dragstart — пользователь начал перемещение карты
  • drag — карта активно перемещается
  • dragend — пользователь отпустил кнопку мыши или палец

Пример подписки на события карты

map.on('dragstart', () => {
  console.log('Начало перетаскивания карты');
});

map.on('drag', () => {
  console.log('Карта перемещается');
});

map.on('dragend', () => {
  console.log('Перетаскивание завершено');
});

Контекст использования drag карты

События drag карты применяются для:

  • синхронизации интерфейсов (например, обновление боковой панели)
  • загрузки данных при остановке перемещения
  • аналитики пользовательского поведения
  • ограничения взаимодействий (например, блокировка UI во время движения)

Различие между drag и move событиями

В Mapbox GL JS существует важное разделение:

  • drag* — инициируются жестами пользователя
  • move* — инициируются любым изменением камеры (включая программное управление)

События move:

  • movestart
  • move
  • moveend

Таким образом:

  • drag всегда является подмножеством move
  • move может происходить без участия пользователя

Это различие критично при построении реактивных интерфейсов.

Перетаскиваемые маркеры

Одним из наиболее частых сценариев является drag маркеров. Для этого используется объект Marker с включённой опцией draggable.

const marker = new mapboxgl.Marker({
  draggable: true
})
.setLngLat([30.5, 50.5])
.addTo(map);

После активации draggable доступны события:

  • dragstart
  • drag
  • dragend

Пример обработки drag маркера

marker.on('dragstart', () => {
  console.log('Маркер начал перемещение');
});

marker.on('drag', () => {
  const lngLat = marker.getLngLat();
  console.log('Текущие координаты:', lngLat);
});

marker.on('dragend', () => {
  const finalPosition = marker.getLngLat();
  console.log('Финальная позиция:', finalPosition);
});

Типичные сценарии использования

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

Drag для пользовательских объектов

Хотя Mapbox GL JS не предоставляет прямого drag API для слоёв (layers), реализуется собственная логика через:

  • map.on('mousedown')
  • map.on('mousemove')
  • map.on('mouseup')
  • map.queryRenderedFeatures

Базовый паттерн перетаскивания слоя

let isDragging = false;
let draggedFeatureId = null;

map.on('mousedown', 'points-layer', (e) => {
  isDragging = true;
  draggedFeatureId = e.features[0].id;
  map.getCanvas().style.cursor = 'grabbing';
});

map.on('mousemove', (e) => {
  if (!isDragging) return;

  const coords = [e.lngLat.lng, e.lngLat.lat];

  // обновление источника данных
  const source = map.getSource('points-source');
  const data = source._data;

  data.features = data.features.map(f => {
    if (f.id === draggedFeatureId) {
      f.geometry.coordinates = coords;
    }
    return f;
  });

  source.setData(data);
});

map.on('mouseup', () => {
  isDragging = false;
  draggedFeatureId = null;
  map.getCanvas().style.cursor = '';
});

Этот подход позволяет реализовать полноценное drag-поведение для GeoJSON-объектов.

Координаты и преобразования при drag

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

Mapbox GL JS автоматически предоставляет:

  • lngLat — географические координаты
  • point — экранные координаты

Конвертация экранных координат

map.on('mousemove', (e) => {
  const screenPoint = e.point;
  const geoPoint = e.lngLat;

  console.log(screenPoint); // пиксели
  console.log(geoPoint);    // долгота/широта
});

При реализации drag логика обычно опирается на lngLat, так как она независима от масштаба.

Drag и производительность рендеринга

Drag-события могут вызывать высокую частоту обновлений (десятки раз в секунду). Это требует оптимизации.

Основные проблемы:

  • частые вызовы setData
  • перерасчёт геометрии
  • перерендеринг слоя

Техника throttling

let lastUpdate = 0;

map.on('mousemove', (e) => {
  if (!isDragging) return;

  const now = Date.now();
  if (now - lastUpdate < 16) return; // ~60 FPS

  lastUpdate = now;

  updateFeaturePosition(e.lngLat);
});

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

function updateFrame(lngLat) {
  requestAnimationFrame(() => {
    updateFeaturePosition(lngLat);
  });
}

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

Drag и взаимодействие с камерой

Во время drag карты изменяется камера (camera transform). Внутренне это включает:

  • центр карты
  • zoom
  • bearing
  • pitch

Пример фиксации состояния камеры

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

  saveState({ center, zoom, bearing });
});

Такая логика применяется для:

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

Ограничение drag-перемещений

Mapbox GL JS позволяет ограничивать область перемещения карты:

const bounds = [
  [29.0, 49.0],
  [31.0, 51.0]
];

map.setMaxBounds(bounds);

Это влияет и на drag:

  • пользователь не сможет увести карту за пределы
  • drag автоматически “упирается” в границы

Drag и inertial movement

После завершения drag может продолжаться инерционное движение (momentum). Оно генерирует:

  • move
  • moveend

но не drag события.

Это важно при разделении логики:

  • drag — пользовательское взаимодействие
  • move — физическая анимация камеры

Сложные сценарии drag-интеракций

Drag с выделением области

let startPoint = null;

map.on('mousedown', (e) => {
  startPoint = e.lngLat;
});

map.on('mousemove', (e) => {
  if (!startPoint) return;

  drawRectangle(startPoint, e.lngLat);
});

map.on('mouseup', () => {
  startPoint = null;
});

Используется в:

  • selection tools
  • spatial queries
  • GIS-инструментах

Drag + фильтрация объектов

Во время drag можно динамически фильтровать данные:

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

  map.setFilter('points-layer', [
    'distance',
    ['get', 'coordinates'],
    center
  ]);
});

Распространённые ошибки при работе с drag

  • обновление источника данных без необходимости на каждый mousemove
  • отсутствие разделения drag и move
  • игнорирование coordinate space (screen vs geo)
  • утечки состояния drag-флагов
  • отсутствие очистки событий при destroy карты

Архитектурные подходы

При проектировании drag-интеракций часто выделяют отдельный слой логики:

  • input layer (mouse/touch events)
  • interaction state machine
  • rendering layer (map sources/layers)

Пример состояния:

const dragState = {
  active: false,
  featureId: null,
  startLngLat: null
};

Такой подход упрощает масштабирование интерактивных функций.

Drag на мобильных устройствах

На touch-устройствах drag события работают через pointer events абстракцию.

Особенности:

  • высокая частота событий
  • необходимость учитывать multi-touch
  • возможные конфликты с pinch-zoom

Рекомендуется:

  • отключать drag при gesture zoom
  • использовать passive event listeners
  • минимизировать вычисления в обработчиках

Итоговая модель поведения drag в Mapbox GL JS

  • drag карты управляет камерой
  • drag маркеров управляет геометрией объектов
  • drag слоёв требует ручной реализации
  • события всегда следуют циклу start → move → end
  • производительность зависит от частоты обновлений и архитектуры обработки событий