Подсветка при наведении реализуется через связку событий мыши,
выборки объектов слоя и динамического изменения состояния фичи через
feature-state. Такой подход обеспечивает плавное обновление
стиля без пересоздания источников данных и без перерисовки слоя
целиком.
Ключевой механизм взаимодействия строится вокруг трёх компонентов:
mousemovequeryRenderedFeaturessetFeatureStateДля корректной работы подсветки требуется GeoJSON-источник с уникальным идентификатором каждой фичи.
map.addSource('cities', {
type: 'geojson',
data: '/data/cities.geojson',
promoteId: 'id'
});
Использование promoteId критично: оно позволяет
обращаться к объектам через стабильный идентификатор, не зависящий от
внутренней структуры данных.
Добавление слоя отображения:
map.addLayer({
id: 'cities-layer',
type: 'fill',
source: 'cities',
paint: {
'fill-color': '#2a6fdb',
'fill-opacity': 0.6
}
});
Состояние подсветки реализуется через хранение текущего активного
featureId.
let hoveredId = null;
При перемещении курсора выполняется запрос к отрисованным объектам слоя:
map.on('mousemove', 'cities-layer', (e) => {
if (!e.features.length) return;
const feature = e.features[0];
const id = feature.id;
if (hoveredId !== null) {
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{ hover: false }
);
}
hoveredId = 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;
});
Изменение визуального представления выполняется через выражения
case в paint-свойствах.
map.setPaintProperty('cities-layer', 'fill-color', [
'case',
['boolean', ['feature-state', 'hover'], false],
'#ffcc00',
'#2a6fdb'
]);
Аналогично можно управлять прозрачностью:
map.setPaintProperty('cities-layer', 'fill-opacity', [
'case',
['boolean', ['feature-state', 'hover'], false],
0.9,
0.5
]);
Когда hover работает сразу по нескольким слоям или требуется
фильтрация по атрибутам, применяется
queryRenderedFeatures:
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['cities-layer']
});
if (!features.length) return;
const feature = features[0];
});
Этот подход полезен при работе с перекрывающимися слоями, когда событие слоя недостаточно точное.
При интенсивном движении курсора возникает большое количество событий
mousemove. Для снижения нагрузки применяется фильтрация
обновлений состояния.
Простейшая форма — проверка изменения идентификатора:
if (id === hoveredId) return;
Более устойчивый подход — использование
requestAnimationFrame:
let pendingId = null;
map.on('mousemove', 'cities-layer', (e) => {
const feature = e.features[0];
pendingId = feature.id;
});
function updateHover() {
if (pendingId !== hoveredId) {
if (hoveredId !== null) {
map.setFeatureState({ source: 'cities', id: hoveredId }, { hover: false });
}
hoveredId = pendingId;
map.setFeatureState({ source: 'cities', id: hoveredId }, { hover: true });
}
requestAnimationFrame(updateHover);
}
updateHover();
При динамическом обновлении GeoJSON все состояния
feature-state сбрасываются. Это требует ручной очистки
логики hover:
map.getSource('cities').setData(newData);
hoveredId = null;
В сложных приложениях дополнительно хранится внешний реестр активных состояний.
При наличии нескольких слоёв одного источника важно учитывать приоритет слоёв.
layers: ['cities-layer', 'cities-border-layer']
Подсветка должна применяться синхронно ко всем связанным слоям через
одно состояние feature-state.
Изменение курсора усиливает визуальную связь с интерактивностью слоя:
map.on('mouseenter', 'cities-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
});
При работе с кластерами требуется отдельная логика, поскольку
кластеры не имеют стабильного feature.id.
map.on('mousemove', 'clusters', (e) => {
const clusterFeature = e.features[0];
const clusterId = clusterFeature.properties.cluster_id;
});
Для раскрытия кластера применяется:
map.getSource('points').getClusterExpansionZoom(clusterId, (err, zoom) => {
map.easeTo({
center: clusterFeature.geometry.coordinates,
zoom
});
});
Hover-логика в таких сценариях часто переносится на некластеризованный слой точек.
События mousemove не применимы для touch-интерфейсов,
поэтому добавляется обработка touchstart:
map.on('touchstart', 'cities-layer', (e) => {
const feature = e.features[0];
map.setFeatureState(
{ source: 'cities', id: feature.id },
{ hover: true }
);
});
Часто используется кратковременное состояние подсветки с последующим таймаутом сброса.
При длительной работе карты накопление feature-state
может приводить к избыточным данным. Для предотвращения деградации
производительности применяется очистка:
map.removeFeatureState({
source: 'cities'
});
Либо точечное удаление:
map.setFeatureState(
{ source: 'cities', id: hoveredId },
{}
);
Корректная работа hover-подсветки зависит от выполнения условия стабильности идентификаторов:
id должен быть уникальнымid не должен изменяться между обновлениями данныхpromoteIdПри нарушении этих условий поведение feature-state
становится непредсказуемым, а визуальные артефакты сохраняются даже
после ухода курсора.