В MapLibre GL JS обработка кликов строится вокруг событийной модели WebGL-карты, где взаимодействие с объектами реализуется через анализ координат курсора и выборку отрисованных геометрий на текущем кадре рендеринга.
Ключевая точка входа — событие:
map.on('click', (e) => {
console.log(e.point); // пиксельные координаты
console.log(e.lngLat); // географические координаты
});
Объект события содержит два фундаментальных источника данных:
e.point — координаты в пикселях относительно контейнера
картыe.lngLat — долгота и широта в момент кликаЭти данные сами по себе не определяют, по какому объекту произошёл клик. Для этого используется выборка объектов слоя.
Основной механизм определения попадания клика в геометрию слоя — метод:
map.queryRenderedFeatures(point, options)
Простейший вариант:
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']
});
if (!features.length) return;
const feature = features[0];
console.log(feature.properties);
});
Параметр layers критически важен для:
При наличии нескольких слоёв в одной точке порядок выбора определяется порядком отрисовки слоёв в стиле карты.
const features = map.queryRenderedFeatures(e.point, {
layers: ['roads', 'buildings', 'pois']
});
Приоритет задаётся массивом:
Это позволяет формировать строгую иерархию интерактивности.
Результат queryRenderedFeatures зависит от типа
слоя:
map.queryRenderedFeatures(e.point, {
layers: ['points']
});
Используется для:
Линейные объекты требуют учёта tolerance-подобной логики, поскольку клик редко попадает строго в пиксель линии.
MapLibre использует буферизацию hit-test на уровне рендера.
Полигоны наиболее чувствительны к кликам, так как проверка осуществляется по области заполнения.
Для повышения интерактивности используется изменение курсора:
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
map.getCanvas().style.cursor = features.length ? 'pointer' : '';
});
Это создаёт поведение, аналогичное DOM-элементам, но применённое к WebGL-слою.
В интерактивных картах часто разделяются два события:
Пример разделения:
map.on('mousemove', (e) => {
const f = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
if (f.length) {
hoveredId = f[0].id;
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: true }
);
}
});
Система feature-state позволяет хранить интерактивное
состояние объектов без модификации источника данных.
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const id = features[0].id;
map.setFeatureState(
{ source: 'cities', id },
{ selected: true }
);
});
Типичные состояния:
hoverselectedactivedisabledКлассический сценарий — отображение всплывающего окна:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(`<strong>${feature.properties.name}</strong>`)
.addTo(map);
});
Ключевой момент: lngLat берётся из события, а не из
feature, чтобы избежать искажений при сложной геометрии.
При использовании кластеризации поведение клика отличается: вместо конечного объекта возвращается кластер.
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
});
}
);
});
Здесь логика разделяется на два уровня:
В одной точке могут находиться несколько объектов разных слоёв.
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);
});
Это используется для:
Для линий и мелких объектов часто применяется расширенная зона захвата через фильтрацию:
map.queryRenderedFeatures(e.point, {
layers: ['roads']
});
Дополнительно можно использовать искусственное расширение области:
const bbox = [
[e.point.x - 5, e.point.y - 5],
[e.point.x + 5, e.point.y + 5]
];
const features = map.queryRenderedFeatures(bbox, {
layers: ['roads']
});
При наложении интерактивных слоёв используется строгая иерархия:
const clickableLayers = ['top-layer', 'middle-layer', 'base-layer'];
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: clickableLayers
});
if (!features.length) return;
const topFeature = features[0];
});
Дополнительно применяется логика остановки дальнейшей обработки:
map.on('click', 'top-layer', (e) => {
e.originalEvent.stopPropagation();
});
При большом количестве объектов критично избегать полного перебора слоёв без фильтрации.
Оптимизационные приёмы:
layersqueryRenderedFeaturesfeature-state вместо перерасчёта
стилейЕсли источник данных обновляется динамически:
map.getSource('cities').setData(newData);
обработчики кликов не требуют пересоздания, так как
queryRenderedFeatures работает с актуальным состоянием
рендера, а не с исходным набором данных.
Интерактивные сценарии часто связывают выбор объекта с изменением положения карты:
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const coords = features[0].geometry.coordinates;
map.flyTo({
center: coords,
zoom: 12
});
});
Такой подход формирует связку:
При использовании WebGL custom layers стандартный
queryRenderedFeatures может быть недоступен, и логика клика
переносится на пользовательскую математику:
e.point в координаты сценыЭто расширяет модель взаимодействия за пределы стандартного слоя данных MapLibre.