Обработка клика

Базовая модель событий клика

В Mapbox GL JS взаимодействие с картой строится на системе событий, аналогичной DOM, но привязанной к географическому контексту. Основное событие для обработки нажатий — click, которое регистрируется через метод map.on.

map.on('click', (e) => {
  console.log(e.lngLat);
});

Объект события содержит ключевую информацию:

  • lngLat — географические координаты точки клика
  • point — пиксельные координаты внутри контейнера карты
  • originalEvent — исходное DOM-событие браузера

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


Определение объектов под курсором

Одна из основных задач обработки клика — определение объектов, находящихся под точкой взаимодействия. Для этого применяется метод queryRenderedFeatures.

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);

  console.log(features);
});

Метод возвращает массив объектов GeoJSON, которые визуально присутствуют в текущем кадре карты.

Фильтрация по слоям

При наличии нескольких слоёв часто требуется ограничить поиск конкретным слоем:

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['cities-layer']
  });

  console.log(features);
});

Такой подход позволяет отделить интерактивные объекты от фоновой графики.


Обработка клика по конкретному слою

Mapbox GL JS поддерживает event-driven модель для слоёв. При включении интерактивности слой может напрямую реагировать на события.

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];

  console.log(feature.properties);
});

В этом режиме:

  • e.features содержит только объекты выбранного слоя
  • фильтрация через queryRenderedFeatures не требуется
  • повышается производительность при большом количестве объектов

Работа с всплывающими окнами

Одним из распространённых сценариев обработки клика является отображение всплывающей информации через Popup.

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];

  new mapboxgl.Popup()
    .setLngLat(e.lngLat)
    .setHTML(`
      <h3>${feature.properties.name}</h3>
      <p>${feature.properties.description}</p>
    `)
    .addTo(map);
});

Ключевые моменты:

  • позиционирование осуществляется через lngLat
  • содержимое формируется динамически из properties
  • popup привязывается к карте методом addTo

Обработка отсутствия объектов

При клике по карте не всегда попадает объект слоя. Это требует проверки результата выборки.

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['cities-layer']
  });

  if (!features.length) {
    return;
  }

  const feature = features[0];
  console.log(feature);
});

Такой подход предотвращает ошибки при обращении к несуществующим элементам массива.


Изменение состояния объектов (Feature State)

Для интерактивных сценариев используется механизм состояния фич — setFeatureState. Он позволяет изменять визуальные свойства без пересоздания источника данных.

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];

  map.setFeatureState(
    {
      source: 'cities-source',
      id: feature.id
    },
    {
      selected: true
    }
  );
});

Стилизация слоя может учитывать это состояние:

'circle-color': [
  'case',
  ['boolean', ['feature-state', 'selected'], false],
  '#ff0000',
  '#3388ff'
]

Снятие выделения при повторном клике

Для управления состоянием часто требуется сброс предыдущего выделения.

let selectedId = null;

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];

  if (selectedId) {
    map.setFeatureState(
      { source: 'cities-source', id: selectedId },
      { selected: false }
    );
  }

  selectedId = feature.id;

  map.setFeatureState(
    { source: 'cities-source', id: selectedId },
    { selected: true }
  );
});

Изменение курсора при наведении и клике

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

map.on('mouseenter', 'cities-layer', () => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities-layer', () => {
  map.getCanvas().style.cursor = '';
});

Такое поведение подчёркивает кликабельность объектов.


Обработка клика по кластеризованным данным

При использовании кластеризации данные группируются, и обработка клика требует дополнительного шага.

map.on('click', 'clusters', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['clusters']
  });

  const clusterId = features[0].properties.cluster_id;

  map.getSource('points').getClusterExpansionZoom(clusterId, (err, zoom) => {
    if (err) return;

    map.easeTo({
      center: features[0].geometry.coordinates,
      zoom
    });
  });
});

Основные этапы:

  • получение cluster_id
  • вычисление уровня масштабирования
  • плавное приближение к кластеру

Приоритет событий и предотвращение конфликтов

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

Для предотвращения конфликта взаимодействий используется логика проверки:

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);

  const target = features.find(f => f.layer.id === 'cities-layer');

  if (!target) return;

  console.log(target);
});

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

Mapbox GL JS унифицирует обработку pointer-событий, однако поведение на тач-устройствах требует учёта задержек и особенностей:

  • событие click может сопровождаться touchend
  • возможны ложные срабатывания при скролле карты
  • рекомендуется избегать тяжёлой логики в обработчиках
map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);
  if (!features.length) return;

  console.log('touch/click interaction');
});

Комбинация клика и других событий

Часто обработка клика дополняется mousemove или mouseenter для создания сложных интерфейсов.

map.on('mousemove', 'cities-layer', (e) => {
  map.getCanvas().style.cursor = 'pointer';
});

map.on('click', 'cities-layer', (e) => {
  const feature = e.features[0];
  console.log(feature);
});

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


Делегирование логики обработки

При усложнении интерфейса обработчики клика часто централизуются:

function handleFeatureClick(feature, lngLat) {
  console.log(feature.properties);
}

map.on('click', 'cities-layer', (e) => {
  handleFeatureClick(e.features[0], e.lngLat);
});

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


Проверка пересечений с несколькими слоями

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

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);

  const grouped = features.reduce((acc, f) => {
    acc[f.layer.id] = acc[f.layer.id] || [];
    acc[f.layer.id].push(f);
    return acc;
  }, {});

  console.log(grouped);
});

Такой подход позволяет обрабатывать сложные сцены, где несколько типов объектов пересекаются в одной точке.


Использование stopPropagation в контексте Mapbox

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

map.on('click', (e) => {
  const features = map.queryRenderedFeatures(e.point);

  if (features.length) {
    // обработка приоритетного объекта
    return;
  }

  console.log('click on empty map');
});

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

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

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

Оптимизация особенно важна при анимациях и частых обновлениях источников данных.


Геометрическая точность и радиус выбора

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

const bbox = [
  [e.point.x - 5, e.point.y - 5],
  [e.point.x + 5, e.point.y + 5]
];

const features = map.queryRenderedFeatures(bbox);

Такой подход создаёт “зону захвата” вокруг курсора, улучшая UX при мелких объектах.