Обработка кликов по объектам

В Mapbox GL JS обработка кликов по объектам строится вокруг событийной модели карты и слоя отрисовки в WebGL. Основной механизм взаимодействия — это определение геометрии под координатами клика и последующая работа с источниками данных (sources) и слоями (layers).

Ключевой метод для обработки кликов:

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

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


Типы кликов и уровни обработки

Обработка кликов может выполняться на нескольких уровнях:

  • глобальный клик по карте (map.on('click'))
  • клик по конкретному слою (map.on('click', layerId, handler))
  • ручной выбор объектов через queryRenderedFeatures

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

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

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


Принцип выборки объектов

В основе выбора объектов лежит пиксельная координата события мыши. Mapbox GL JS преобразует экранные координаты в проверку пересечения с отрисованными тайлами.

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

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


Работа с feature-state

Для управления состоянием объектов без изменения исходного GeoJSON применяется механизм feature-state. Он особенно полезен при обработке кликов, когда требуется подсветка или изменение визуального состояния.

Установка состояния:

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

Использование состояния в стиле слоя:

map.setPaintProperty('cities-layer', 'circle-color', [
    'case',
    ['boolean', ['feature-state', 'selected'], false],
    '#ff0000',
    '#3388ff'
]);

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


Обработка перекрывающихся объектов

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

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

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

Первый элемент массива соответствует верхнему визуальному слою, что соответствует принципу painter’s algorithm.

Для более точного контроля применяется фильтрация по слоям или анализ геометрии:

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

Использование bounding box вокруг клика

Вместо точечного попадания можно использовать небольшую область вокруг курсора:

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

const features = map.queryRenderedFeatures(bbox);

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


Разделение логики по типам геометрии

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

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

    features.forEach((f) => {
        switch (f.geometry.type) {
            case 'Point':
                console.log('точка');
                break;
            case 'LineString':
                console.log('линия');
                break;
            case 'Polygon':
                console.log('полигон');
                break;
        }
    });
});

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


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

Mapbox GL JS использует DOM-события поверх canvas, поэтому требуется управление propagation:

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

Это предотвращает срабатывание глобального обработчика карты при клике по слою.


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

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

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

    const filtered = features.filter((f) => {
        return f.properties.population > 100000;
    });

    console.log(filtered);
});

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


Работа с курсором и UX-индикацией

Изменение курсора при наведении на интерактивные объекты выполняется через события mouseenter и mouseleave:

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

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

Такая индикация повышает предсказуемость взаимодействия с картой.


Использование popup при клике

Один из стандартных сценариев — отображение всплывающей информации:

map.on('click', 'cities-layer', (e) => {
    const coordinates = e.features[0].geometry.coordinates.slice();
    const description = e.features[0].properties.name;

    new mapboxgl.Popup()
        .setLngLat(coordinates)
        .setHTML(description)
        .addTo(map);
});

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


Обработка кликов на динамических слоях

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

map.addLayer({
    id: 'dynamic-layer',
    type: 'circle',
    source: 'dynamic-source'
});

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

Отсутствие слоя в момент регистрации события приводит к отсутствию реакции на клики.


Приоритет слоёв и порядок обработки

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

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

map.moveLayer('layer-id', 'before-layer-id');

или

map.moveLayer('layer-id');

Контроль порядка слоёв позволяет точно определять, какой объект будет выбран при клике.


Оптимизация обработки кликов

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

  • ограничение layers в queryRenderedFeatures
  • использование feature-state вместо пересоздания GeoJSON
  • сокращение количества активных интерактивных слоёв
  • отказ от глобального click в пользу layer-based событий
map.on('click', (e) => {
    const features = map.queryRenderedFeatures(e.point, {
        layers: ['optimized-layer']
    });
});

Обработка drag-сценариев и отличия от click

Событие click в Mapbox GL JS фактически является комбинацией mousedown и mouseup. При смещении курсора событие может не срабатывать.

Для различения используется логика:

let startPoint = null;

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

map.on('mouseup', (e) => {
    const dx = Math.abs(e.point.x - startPoint.x);
    const dy = Math.abs(e.point.y - startPoint.y);

    if (dx < 3 && dy < 3) {
        const features = map.queryRenderedFeatures(e.point);
        console.log(features);
    }
});

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


Работа с кластеризованными объектами

При использовании кластеризации поведение кликов меняется: сначала определяется кластер, затем дочерние объекты.

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

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

    map.getSource('points').getClusterExpansionZoom(clusterId, (err, zoom) => {
        map.easeTo({
            center: features[0].geometry.coordinates,
            zoom
        });
    });
});

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


Контроль интерактивности через filter

Слои могут динамически ограничивать интерактивность:

map.setFilter('cities-layer', ['==', ['get', 'active'], true]);

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