Подсветка при наведении

Подсветка при наведении реализуется через связку событий мыши, выборки объектов слоя и динамического изменения состояния фичи через feature-state. Такой подход обеспечивает плавное обновление стиля без пересоздания источников данных и без перерисовки слоя целиком.

Ключевой механизм взаимодействия строится вокруг трёх компонентов:

  • обработка события mousemove
  • выборка объектов через queryRenderedFeatures
  • управление состоянием через setFeatureState

Подготовка источника данных и слоя

Для корректной работы подсветки требуется 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
  }
});

Базовая логика hover-состояния

Состояние подсветки реализуется через хранение текущего активного 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
]);

Использование queryRenderedFeatures для сложных сценариев

Когда 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.


Курсор и UX-обратная связь

Изменение курсора усиливает визуальную связь с интерактивностью слоя:

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 становится непредсказуемым, а визуальные артефакты сохраняются даже после ухода курсора.