Drag and drop маркеров

Mapbox GL JS построена вокруг WebGL-рендеринга и модели объектов, в которой маркеры, слои и источники данных обновляются в реальном времени. Поддержка drag-and-drop для маркеров реализуется через встроенный механизм Marker, позволяющий изменять географические координаты объекта с последующим синхронным обновлением состояния карты.

Основной способ включения перетаскивания — использование опции draggable: true при создании маркера.

mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';

const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v12',
  center: [69.2401, 41.2995],
  zoom: 10
});

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

Внутри Marker используется DOM-элемент, который получает обработчики pointer-событий. При активации draggable библиотека автоматически регистрирует события мыши и touch-интерфейса.

Получение координат после перемещения

После завершения перетаскивания координаты извлекаются через метод getLngLat().

function onDragEnd() {
  const lngLat = marker.getLngLat();

  console.log('Longitude:', lngLat.lng);
  console.log('Latitude:', lngLat.lat);
}

marker.on('dragend', onDragEnd);

Событие dragend фиксирует финальную позицию, тогда как drag позволяет отслеживать непрерывное движение.

Обработка событий drag

Механизм событий включает три основных состояния:

  • dragstart — начало перемещения
  • drag — активное перемещение
  • dragend — завершение перемещения
marker.on('dragstart', () => {
  console.log('start dragging');
});

marker.on('drag', () => {
  const pos = marker.getLngLat();
  console.log(pos);
});

marker.on('dragend', () => {
  const final = marker.getLngLat();
  console.log('final position:', final);
});

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

Привязка маркера к данным состояния

В прикладных сценариях маркер часто связан с внешним состоянием (например, объектом в базе или GeoJSON-слоем). При перетаскивании обновляется источник данных.

const state = {
  point: [69.2401, 41.2995]
};

marker.on('dragend', () => {
  const { lng, lat } = marker.getLngLat();

  state.point = [lng, lat];
});

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

Работа с несколькими draggable-маркерами

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

const points = [
  [69.2, 41.3],
  [69.25, 41.31],
  [69.28, 41.29]
];

const markers = points.map((coords) => {
  const m = new mapboxgl.Marker({ draggable: true })
    .setLngLat(coords)
    .addTo(map);

  m.on('dragend', () => {
    const pos = m.getLngLat();
    console.log('updated:', pos);
  });

  return m;
});

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

Кастомные DOM-маркеры и drag behavior

Маркер может содержать произвольный HTML-элемент, что позволяет создавать сложные интерфейсы.

const el = document.createElement('div');
el.className = 'custom-marker';
el.innerHTML = '<div class="pin"></div>';

const customMarker = new mapboxgl.Marker({
  element: el,
  draggable: true
})
.setLngLat([69.2401, 41.2995])
.addTo(map);

В этом случае важно учитывать CSS-ограничения: pointer-events, transform-origin и position, поскольку они влияют на корректность drag-поведения.

Синхронизация с GeoJSON-источниками

При работе со слоями GeoJSON маркер часто выступает визуальным представлением feature. После drag необходимо обновлять источник через setData.

const geojson = {
  type: 'FeatureCollection',
  features: [{
    type: 'Feature',
    geometry: {
      type: 'Point',
      coordinates: [69.2401, 41.2995]
    }
  }]
};

map.on('load', () => {
  map.addSource('point', {
    type: 'geojson',
    data: geojson
  });

  map.addLayer({
    id: 'point-layer',
    type: 'circle',
    source: 'point',
    paint: {
      'circle-radius': 6,
      'circle-color': '#ff0000'
    }
  });

  marker.on('dragend', () => {
    const pos = marker.getLngLat();

    geojson.features[0].geometry.coordinates = [pos.lng, pos.lat];
    map.getSource('point').setData(geojson);
  });
});

Такой подход обеспечивает синхронное обновление слоя без пересоздания карты.

Ограничение перемещения маркера (bounds)

Часто требуется ограничить область перемещения. Это реализуется через проверку координат в обработчике drag.

const bounds = [
  [69.0, 41.0],
  [69.5, 41.5]
];

marker.on('drag', () => {
  const pos = marker.getLngLat();

  const lng = Math.max(bounds[0][0], Math.min(bounds[1][0], pos.lng));
  const lat = Math.max(bounds[0][1], Math.min(bounds[1][1], pos.lat));

  marker.setLngLat([lng, lat]);
});

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

Оптимизация частых обновлений при drag

Событие drag генерируется на каждом кадре. При привязке к тяжелым операциям (рендер UI, запросы) используется requestAnimationFrame.

let frame;

marker.on('drag', () => {
  if (frame) return;

  frame = requestAnimationFrame(() => {
    const pos = marker.getLngLat();
    updateUI(pos);
    frame = null;
  });
});

Это снижает нагрузку и предотвращает блокировку интерфейса при интенсивном перемещении.

Интеграция с внешними системами состояния

В архитектурах с глобальным состоянием (Redux, Zustand или аналогами) маркер часто диспатчит события вместо прямого обновления данных.

marker.on('dragend', () => {
  const { lng, lat } = marker.getLngLat();

  dispatch({
    type: 'UPDATE_MARKER_POSITION',
    payload: { lng, lat }
  });
});

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