Обработка событий маркера

В Mapbox GL JS маркеры представляют собой интерактивные DOM-элементы, привязанные к координатам на карте, и именно через них реализуется значительная часть пользовательского взаимодействия: клики, перетаскивание, наведение, открытие всплывающих окон и интеграция с внешними состояниями приложения. Обработка событий маркера строится на сочетании стандартной DOM-модели событий и внутренней логики карты, что требует понимания как поведения самого маркера, так и особенностей событийного потока внутри карты.

Маркер в Mapbox GL JS не является отдельным графическим примитивом WebGL. Он создаётся как HTML-элемент, размещаемый поверх карты в отдельном слое DOM. Это определяет ключевую особенность: события маркера обрабатываются через стандартные браузерные механизмы addEventListener, а не через WebGL-подсистему карты.

Каждый маркер содержит:

  • DOM-элемент (по умолчанию div)
  • координаты привязки к карте
  • опциональные настройки перетаскивания
  • внутренние обработчики синхронизации позиции при движении карты

Такой подход делает маркеры гибкими, но накладывает ограничения: обработка событий зависит от правил всплытия DOM-событий и взаимодействия с контейнером карты.

Базовые события клика

Наиболее распространённый сценарий — обработка клика по маркеру. Поскольку маркер является DOM-элементом, стандартный обработчик добавляется напрямую:

const markerElement = document.createElement('div');
markerElement.className = 'marker';

markerElement.addEventListener('click', (e) => {
    console.log('Клик по маркеру');
});

Клик обрабатывается до того, как событие может быть интерпретировано картой. Это важно: если не контролировать всплытие, клик по маркеру может одновременно вызвать обработчики карты, например map.on('click').

Для предотвращения нежелательного распространения события используется:

markerElement.addEventListener('click', (e) => {
    e.stopPropagation();
});

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

Интеграция с событиями карты

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

markerElement.addEventListener('click', () => {
    popup.setLngLat([lng, lat]).addTo(map);
});

Однако при сложных интерфейсах важно учитывать конкуренцию событий: карта может реагировать на тот же клик (панорамирование, зум, обработка feature query). Поэтому часто применяется стратегия фильтрации:

map.on('click', (e) => {
    const features = map.queryRenderedFeatures(e.point);
    if (features.length === 0) {
        popup.remove();
    }
});

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

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

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

const marker = new mapboxgl.Marker({
    draggable: true
});

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

  • dragstart
  • drag
  • dragend

dragstart

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

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

drag

Срабатывает непрерывно во время перемещения. Часто применяется для обновления координат UI:

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

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

dragend

Фиксирует финальную позицию:

marker.on('dragend', () => {
    const position = marker.getLngLat();
    savePosition(position);
});

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

Наведение и пользовательские эффекты

Хотя Mapbox GL JS не предоставляет специализированных событий hover для маркеров, они легко реализуются через DOM-события:

markerElement.addEventListener('mouseenter', () => {
    markerElement.classList.add('hover');
});

markerElement.addEventListener('mouseleave', () => {
    markerElement.classList.remove('hover');
});

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

Важно учитывать, что на сенсорных устройствах mouseenter и mouseleave могут не срабатывать, поэтому требуется альтернативная логика через touchstart.

Обработка касаний (touch events)

На мобильных устройствах основным источником взаимодействия становятся события:

  • touchstart
  • touchmove
  • touchend

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

markerElement.addEventListener('touchstart', (e) => {
    console.log('Касание маркера');
});

Особенность заключается в том, что touch-события часто конфликтуют с жестами карты (панорамирование и масштабирование). Поэтому требуется явное управление:

markerElement.addEventListener('touchstart', (e) => {
    e.stopPropagation();
});

Без этого карта может перехватывать жест, что делает взаимодействие с маркером нестабильным.

Делегирование событий и производительность

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

Подход основан на общем контейнере:

map.getCanvasContainer().addEventListener('click', (e) => {
    const target = e.target;
    if (target.classList.contains('marker')) {
        console.log('Клик по маркеру через делегирование');
    }
});

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

Пользовательские HTML-маркеры и события

Mapbox GL JS позволяет создавать полностью кастомные маркеры:

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

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

В этом случае обработка событий полностью контролируется разработчиком. Это даёт гибкость, но требует ручного управления:

  • клики
  • hover-состояния
  • drag-логика (если реализуется вручную)
  • анимации

Особенно важно правильно учитывать pointer-events в CSS, иначе события могут не доходить до элемента:

.custom-marker {
    pointer-events: auto;
}

Конфликты событий и приоритеты

События маркеров часто конфликтуют с событиями карты:

  • клик по маркеру vs клик по карте
  • drag маркера vs pan карты
  • scroll/zoom vs hover UI

Для управления приоритетами используется комбинация:

  • stopPropagation()
  • отключение взаимодействия карты через map.dragPan.disable()
  • временное блокирование событий карты

Пример:

marker.on('dragstart', () => {
    map.dragPan.disable();
});

marker.on('dragend', () => {
    map.dragPan.enable();
});

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

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

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

  • обновление координат в store (Redux, Zustand)
  • отправка данных на сервер
  • обновление URL параметров
  • управление списками объектов

Пример синхронизации:

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

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

Удаление обработчиков и управление жизненным циклом

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

function createMarker() {
    const el = document.createElement('div');

    function onClick() {
        console.log('click');
    }

    el.addEventListener('click', onClick);

    return {
        el,
        destroy() {
            el.removeEventListener('click', onClick);
        }
    };
}

При удалении маркера из карты необходимо вызывать очистку:

marker.remove();

И дополнительно очищать связанные ресурсы.

Продвинутые сценарии событий

В сложных приложениях маркеры становятся частью событийной системы:

  • группировка маркеров
  • кластеризация с проксированием событий
  • синхронизация с WebSocket
  • реактивные обновления положения

Например, при обновлении данных в реальном времени:

socket.on('update', (data) => {
    marker.setLngLat(data.coords);
});

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

Итоговые паттерны взаимодействия

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

  • DOM-driven события: обработка через addEventListener
  • Map-driven события: синхронизация с map.on
  • State-driven события: управление через глобальное состояние
  • Hybrid-модель: комбинация всех подходов

Каждый из этих подходов определяет, где именно обрабатываются события маркеров — на уровне DOM, карты или приложения.