Интерактивное поведение при наведении курсора является одним из ключевых механизмов взаимодействия с картографическими интерфейсами. В Mapbox GL JS hover-эффекты реализуются через событийную модель WebGL-рендерера и API слоёв, позволяя динамически изменять стиль объектов, отображать всплывающие окна, а также управлять состоянием фич без перерисовки всей сцены.
Hover-логика в Mapbox GL JS строится вокруг событий мыши, привязанных к слоям карты:
mouseenter — курсор вошёл в область слояmousemove — движение курсора внутри слояmouseleave — курсор покинул слойmouseover — аналогично mouseenter, но с
особенностями всплытия событийmouseout — аналогично mouseleaveБазовая структура обработки событий:
map.on('mouseenter', 'cities-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
});
Изменение курсора является самым простым визуальным индикатором интерактивности слоя и часто используется как первый уровень hover-интеракции.
Одним из наиболее распространённых подходов является динамическое изменение paint-параметров слоя при наведении.
map.on('mousemove', 'cities-layer', (e) => {
if (e.features.length > 0) {
const feature = e.features[0];
map.setPaintProperty('cities-layer', 'fill-color', [
'case',
['==', ['get', 'id'], feature.properties.id],
'#ffcc00',
'#3388ff'
]);
}
});
Такой подход работает, но имеет ограничение: он изменяет стиль всего слоя, а не отдельных фич логически. При большом количестве объектов может возникать избыточная переработка стиля.
Более производительный и масштабируемый способ — использование состояния фич.
Mapbox GL JS предоставляет механизм feature-state,
который позволяет хранить временные состояния объектов без изменения
исходного GeoJSON или стиля слоя.
let hoveredId = null;
map.on('mousemove', 'cities-layer', (e) => {
if (e.features.length > 0) {
const feature = e.features[0];
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
}
hoveredId = feature.id;
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: true }
);
}
});
map.on('mouseleave', 'cities-layer', () => {
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
}
hoveredId = null;
});
map.addLayer({
id: 'cities-layer',
type: 'circle',
source: 'cities',
paint: {
'circle-radius': 6,
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ff3b3b',
'#3b82f6'
]
}
});
Hover-эффекты часто зависят от определения объекта под курсором. Для
этого используется queryRenderedFeatures.
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
});
Этот метод позволяет работать даже с перекрывающимися слоями и сложной визуализацией.
Hover часто сопровождается отображением popup-элементов.
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
});
map.on('mousemove', 'cities-layer', (e) => {
const feature = e.features[0];
map.getCanvas().style.cursor = 'pointer';
popup
.setLngLat(feature.geometry.coordinates)
.setHTML(`<strong>${feature.properties.name}</strong>`)
.addTo(map);
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});
Особенность подхода заключается в необходимости синхронизации popup с движением мыши и предотвращении «дрожания» при частом обновлении координат.
В реальных приложениях часто присутствуют несколько интерактивных слоёв. Hover-логика должна учитывать приоритеты слоёв.
const hoverLayers = ['cities-layer', 'regions-layer', 'roads-layer'];
hoverLayers.forEach((layer) => {
map.on('mousemove', layer, (e) => {
const feature = e.features[0];
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', layer, () => {
map.getCanvas().style.cursor = '';
});
});
При пересечении слоёв важно учитывать порядок рендеринга, так как верхний слой может «перехватывать» события.
Hover-события могут вызываться десятки раз в секунду, поэтому критично минимизировать нагрузку:
mousemovefeature.idfeature-state вместо
setPaintPropertyТипичный анти-паттерн:
map.on('mousemove', (e) => {
map.setPaintProperty('layer', 'fill-color', generateComplexEx * pression(e));
});
Такой подход приводит к постоянной переработке стиля слоя и ухудшению FPS.
filter для выделения фичДля сложных геометрий (линии, полигоны) hover часто требует увеличения области попадания:
map.addLayer({
id: 'roads-hover',
type: 'line',
source: 'roads',
paint: {
'line-width': [
'case',
['boolean', ['feature-state', 'hover'], false],
6,
2
],
'line-color': '#ff9900'
}
});
Такой подход создаёт визуальное «утолщение» линии при наведении, улучшая UX без изменения геометрии.
Hover часто используется вместе с кликом, что требует разделения состояний:
map.setFeatureState({ source: 'cities', id }, {
hover: true,
selected: false
});
И в стиле:
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#10b981',
['boolean', ['feature-state', 'hover'], false],
'#f59e0b',
'#3b82f6'
]
При быстром перемещении курсора возможны состояния гонки, когда несколько фич остаются в hover-состоянии. Решение заключается в централизованном хранении текущего hoveredId и обязательном сбросе предыдущего состояния перед установкой нового.
При работе с GeoJSON источниками важно учитывать наличие
id у каждой фичи:
map.addSource('cities', {
type: 'geojson',
data: geojsonData,
generateId: true
});
Без id механизм feature-state становится
неприменимым, что вынуждает использовать менее эффективные подходы.
При обновлении источников через setData hover-состояния
могут сбрасываться, поэтому:
feature-stateТиповая структура hover-обработки в сложных приложениях:
feature-state как основного
механизмаТакой подход обеспечивает стабильную производительность даже при большом количестве интерактивных объектов на карте, построенной на Mapbox инфраструктуре.