Получение feature под курсором

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

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


Основной механизм: queryRenderedFeatures

Метод queryRenderedFeatures возвращает массив объектов GeoJSON, которые в данный момент отрисованы в заданной области экрана.

Сигнатура:

map.queryRenderedFeatures(point, options)
  • point — координаты в пикселях { x, y } или прямоугольная область { x1, y1, x2, y2 }
  • options — фильтрация по слоям, фильтрам и типам

Простейший пример получения объекта под курсором:

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

  console.log(features);
});

Каждый элемент массива содержит:

  • id (если задан)
  • geometry
  • properties
  • layer
  • source
  • state (если используется feature-state API)

Ограничение выборки слоями

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

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

  console.log(features);
});

Это критически важно для производительности, особенно при наличии десятков слоёв.


Получение объекта при клике

Наиболее распространённый сценарий — выбор объекта по клику.

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

  if (!features.length) return;

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

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


Наведение курсора и hover-эффекты

Для реализации hover-эффектов используется событие mousemove в сочетании с изменением состояния feature.

let hoveredId = null;

map.on('mousemove', 'parcels', (e) => {
  if (e.features.length > 0) {
    const feature = e.features[0];

    if (hoveredId !== null) {
      map.setFeatureState(
        { source: 'parcels-source', id: hoveredId },
        { hover: false }
      );
    }

    hoveredId = feature.id;

    map.setFeatureState(
      { source: 'parcels-source', id: hoveredId },
      { hover: true }
    );
  }
});

Стили слоя:

paint: {
  'fill-color': [
    'case',
    ['boolean', ['feature-state', 'hover'], false],
    '#ff8800',
    '#0088ff'
  ]
}

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


Использование point vs bbox

queryRenderedFeatures поддерживает два режима:

Точка (point)

Используется для hover и кликов:

map.queryRenderedFeatures(e.point);

Область (bounding box)

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

map.queryRenderedFeatures([
  [x1, y1],
  [x2, y2]
]);

Пример выделения области:

map.on('mouseup', (e) => {
  const bbox = [
    startPoint,
    [e.point.x, e.point.y]
  ];

  const features = map.queryRenderedFeatures(bbox, {
    layers: ['parcels']
  });

  console.log(features);
});

Работа с несколькими слоями одновременно

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

const features = map.queryRenderedFeatures(e.point, {
  layers: ['roads', 'buildings', 'water']
});

В этом случае важно учитывать:

  • порядок слоёв влияет на порядок результатов
  • объекты разных типов могут иметь пересекающиеся координаты
  • необходимо фильтровать по feature.layer.id при обработке

Использование filter-предикатов

Mapbox GL JS позволяет дополнительно фильтровать результат по свойствам:

const features = map.queryRenderedFeatures(e.point, {
  layers: ['cities'],
  filter: ['==', ['get', 'type'], 'capital']
});

Чаще используется фильтрация уже после получения данных:

const features = map.queryRenderedFeatures(e.point, {
  layers: ['cities']
}).filter(f => f.properties.population > 1000000);

Оптимизация производительности

При интенсивном движении мыши (mousemove) важно учитывать частоту вызовов.

Дебаунс через requestAnimationFrame

let lastEvent = null;

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

function tick() {
  if (lastEvent) {
    const features = map.queryRenderedFeatures(lastEvent.point, {
      layers: ['cities']
    });

    // обработка
  }

  requestAnimationFrame(tick);
}

tick();

Ограничение слоёв

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


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

При использовании кластеризации (cluster: true) объект под курсором может быть кластером, а не исходной сущностью.

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

  const cluster = features[0];

  const clusterId = cluster.properties.cluster_id;

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

Использование feature-state при наведении

Feature-state позволяет изменять внешний вид объектов без изменения данных источника.

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

И в стиле:

'fill-opacity': [
  'case',
  ['boolean', ['feature-state', 'selected'], false],
  1,
  0.5
]

Это особенно эффективно при тысячах объектов.


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

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

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

const topFeature = features.reduce((prev, curr) => {
  return curr.layer.id > prev.layer.id ? curr : prev;
});

Альтернативный подход — явная настройка порядка слоёв через moveLayer.


Получение всех объектов без фильтрации по слоям

Иногда требуется получить полный стек объектов:

const features = map.queryRenderedFeatures(e.point);

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


Сравнение queryRenderedFeatures и querySourceFeatures

Важно различать два метода:

queryRenderedFeatures

Работает с уже отрисованными объектами, учитывает стиль и видимость.

querySourceFeatures

Работает с данными источника до рендеринга:

map.querySourceFeatures('parcels-source', {
  sourceLayer: 'parcels'
});

Используется для анализа данных, но не учитывает текущую визуализацию.


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

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

map.on('mousemove', 'parcels', () => {
  map.getCanvas().style.cursor = 'pointer';
});

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

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


Типичные ошибки при работе с feature под курсором

  • отсутствие ограничения по слоям приводит к непредсказуемым результатам
  • использование queryRenderedFeatures вне событий карты (без координат)
  • попытка работать с feature-state без уникальных id
  • игнорирование порядка слоёв при перекрытии объектов
  • частые вызовы без оптимизации при mousemove

Интеграция с всплывающими окнами

Получение feature под курсором часто используется для Popup:

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

  if (!feature) return;

  new mapboxgl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`<div>${feature.properties.name}</div>`)
    .addTo(map);
});

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

При изменении zoom и pitch набор возвращаемых features может меняться, так как:

  • объекты появляются/исчезают в зависимости от зума
  • 3D-слои (extrusions) могут перекрывать 2D-слои
  • порядок отрисовки влияет на результат выборки

Использование в сложных интерфейсах

В более сложных системах queryRenderedFeatures становится основой для:

  • построения аналитических панелей
  • геопространственного фильтра
  • выделения множественных объектов
  • синхронизации с внешними UI-компонентами
  • построения инструментов редактирования геометрии

Логика всегда опирается на один принцип: выбор происходит не в данных, а в текущем рендере карты.