Одной из ключевых возможностей Mapbox GL JS является выбор геообъектов непосредственно под курсором мыши. Эта функциональность лежит в основе большинства интерактивных карт: подсветка объектов при наведении, отображение всплывающих карточек, построение сценариев выбора объектов, фильтрация и анализ данных в реальном времени.
В Mapbox GL JS работа с объектами под курсором строится вокруг метода
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 (если задан)geometrypropertieslayersourcestate (если используется 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-эффектов используется событие
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, так как не требует перерисовки источника.
queryRenderedFeatures поддерживает два режима:
Используется для hover и кликов:
map.queryRenderedFeatures(e.point);
Используется для выделения рамкой:
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 при
обработке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) важно
учитывать частоту вызовов.
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 позволяет изменять внешний вид объектов без изменения данных источника.
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);
Такой вызов полезен для отладки или построения инструментов анализа карты, но в продакшене используется редко из-за нагрузки.
Важно различать два метода:
Работает с уже отрисованными объектами, учитывает стиль и видимость.
Работает с данными источника до рендеринга:
map.querySourceFeatures('parcels-source', {
sourceLayer: 'parcels'
});
Используется для анализа данных, но не учитывает текущую визуализацию.
Изменение курсора часто используется совместно с выборкой объектов:
map.on('mousemove', 'parcels', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'parcels', () => {
map.getCanvas().style.cursor = '';
});
Это визуально связывает интерактивность с наличием feature под курсором.
queryRenderedFeatures вне событий карты
(без координат)idmousemoveПолучение 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 может меняться, так как:
В более сложных системах queryRenderedFeatures
становится основой для:
Логика всегда опирается на один принцип: выбор происходит не в данных, а в текущем рендере карты.