События слоев

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

Слои в системе рендеринга Mapbox представляют собой абстракции над источниками данных (sources), и именно слой становится точкой привязки интерактивности. События не генерируются самими данными напрямую — они вычисляются через попадание курсора в область пикселей, соответствующих отрисованным геометриям слоя.


Привязка событий к слоям

Основной паттерн обработки событий строится вокруг метода map.on, где вторым аргументом указывается идентификатор слоя.

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

Такой обработчик срабатывает только в случае попадания события в геометрию слоя cities-layer. Объект события содержит массив features, включающий все объекты, удовлетворяющие попаданию в пиксель события.

Ключевые типы событий, применяемые к слоям:

  • click
  • dblclick
  • mouseenter
  • mouseleave
  • mousemove
  • mousedown
  • mouseup
  • touchstart
  • touchend

Геометрическое определение попадания

Определение того, попадает ли курсор в слой, основано на функции выборки отрисованных объектов:

  • анализируется текущий viewport;
  • вычисляется список рендеренных фич;
  • проверяется пересечение с координатой пикселя события.

Эквивалентом низкоуровневой операции является:

map.queryRenderedFeatures(point, {
  layers: ['cities-layer']
});

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


События наведения курсора

mouseenter и mouseleave

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

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

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

Семантика отличается от DOM-событий: переход фиксируется не по элементам HTML, а по результатам WebGL hit-testing.

mousemove

Событие mousemove внутри слоя генерируется при каждом движении курсора над геометрией слоя:

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

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


Клик по слоям и извлечение объектов

Событие click является центральным для взаимодействия с объектами карты.

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

При совпадении нескольких объектов возвращается массив features, отсортированный по визуальной приоритетности слоёв и порядку отрисовки.

Особенность модели заключается в том, что событие не связано с DOM-элементами, а формируется на основе GPU-буфера.


Взаимодействие с несколькими слоями

Один и тот же источник данных может использоваться в нескольких слоях (fill, line, symbol), и события могут пересекаться.

map.on('click', ['fill-layer', 'line-layer'], (e) => {
  console.log(e.features);
});

При этом:

  • событие срабатывает, если хотя бы один слой содержит объект под курсором;
  • features содержит объекты всех указанных слоёв;
  • порядок определяется визуальным стеком.

Состояние фич (feature state) и события

События слоёв часто используются вместе с состоянием объектов:

map.on('mousemove', 'cities-layer', (e) => {
  if (e.features.length > 0) {
    map.setFeatureState(
      { source: 'cities', id: e.features[0].id },
      { hover: true }
    );
  }
});

Состояние влияет на стили слоя через выражения:

'paint': {
  'circle-color': [
    'case',
    ['boolean', ['feature-state', 'hover'], false],
    '#ff0000',
    '#3388ff'
  ]
}

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


Задержки и повторные события

События mouseenter и mousemove не гарантируют стабильную частоту вызовов, поскольку зависят от:

  • загрузки и отрисовки тайлов;
  • сложности геометрии;
  • производительности GPU;
  • плотности данных в viewport.

Повторные mousemove могут приходить без изменения features, поэтому часто применяется сравнение идентификаторов:

let lastId = null;

map.on('mousemove', 'cities-layer', (e) => {
  const id = e.features[0]?.id;
  if (id !== lastId) {
    lastId = id;
  }
});

Отмена поведения и всплытие событий

События слоёв не используют классическое DOM-всплытие. Однако существует приоритет обработки:

  • слой выше в визуальном порядке имеет приоритет;
  • события не «всплывают» к карте, если перехвачены слоем;
  • обработчики карты (map.on('click', ...)) получают событие только при отсутствии совпадений слоёв.

Touch-события

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

map.on('touchstart', 'cities-layer', (e) => {
  console.log(e.features);
});

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

  • touchstart часто заменяет click;
  • возможна задержка между касанием и генерацией события;
  • мультитач приводит к множественным координатам событий.

Практика фильтрации слоёв при событиях

События часто комбинируются с фильтрацией через filter слоя:

map.setFilter('cities-layer', ['==', ['get', 'type'], 'capital']);

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


Взаимодействие с popup и overlay логикой

События слоёв часто используются для привязки всплывающих окон:

map.on('click', 'cities-layer', (e) => {
  new mapboxgl.Popup()
    .setLngLat(e.features[0].geometry.coordinates)
    .setHTML(e.features[0].properties.name)
    .addTo(map);
});

Поведение зависит от точности геометрии и zoom-уровня, поскольку hit-test основан на пиксельной интерпретации.


Производственные ограничения событийной модели

При высокой плотности данных возникают ограничения:

  • деградация производительности при частых mousemove;
  • рост стоимости hit-testing;
  • увеличение нагрузки при использовании сложных фильтров;
  • необходимость дебаунса событий.

Типовой подход оптимизации:

let timeout;

map.on('mousemove', 'cities-layer', (e) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    // обработка
  }, 50);
});

Отличие layer events от map events

Map-level события:

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

Layer-level события:

map.on('click', 'layer-id', (e) => {});

Различия:

  • map-level события не зависят от слоёв;
  • layer-level события требуют hit-test по конкретному слою;
  • layer-level события возвращают features;
  • map-level события возвращают координаты без привязки к данным.

Работа с несколькими источниками данных

При использовании composite sources событие может включать фичи из разных тайловых источников. В таком случае:

  • объединяются результаты hit-test;
  • сохраняется порядок визуальной приоритетности;
  • возможны дубликаты логических сущностей при разных zoom-уровнях.

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

Интерактивность слоя определяется наличием:

  • события map.on(..., layer, handler)
  • или параметра interactive: true (в старых конфигурациях)
  • корректного источника данных с id у features

Без идентификаторов feature.id некоторые операции setFeatureState становятся недоступными, хотя события продолжают работать.


Связь событий и рендер-пайплайна

События слоёв генерируются после стадии рендера, где:

  1. загружаются тайлы;
  2. строится GPU буфер;
  3. выполняется отрисовка слоя;
  4. выполняется hit-testing;
  5. формируется объект события.

Это означает, что события всегда отражают уже отрисованное состояние, а не исходные данные напрямую.