Hover эффекты

В OpenLayers взаимодействие «наведение курсора» реализуется не как отдельный встроенный hover-ивент для объектов, а через низкоуровневое событие карты pointermove. Оно срабатывает при каждом перемещении указателя мыши или пера и служит базовой точкой для определения объектов под курсором.

map.on('pointermove', function (event) {
  // event.coordinate — координаты в системе карты
  // event.pixel — пиксель на экране
});

Основная особенность подхода заключается в том, что hover не является сущностью слоя или объекта. Это вычисляемое состояние, которое определяется в момент движения указателя.


Определение объекта под курсором

Ключевой механизм — проверка попадания пикселя в геометрию объектов слоя. Для этого используется forEachFeatureAtPixel.

map.on('pointermove', function (event) {
  map.forEachFeatureAtPixel(event.pixel, function (feature, layer) {
    console.log(feature.get('name'));
  });
});

Функция перебирает все объекты, находящиеся под пикселем курсора. При наличии нескольких слоёв возвращается первый найденный объект в порядке отрисовки.

Дополнительные параметры позволяют управлять чувствительностью:

map.forEachFeatureAtPixel(event.pixel, function (feature) {
  // логика hover
}, {
  hitTolerance: 5
});

hitTolerance расширяет область попадания, что критично для тонких линий и мелких точек.


Реализация подсветки объекта

Наиболее распространённый hover-эффект — временное изменение стиля объекта при наведении. Это реализуется через сохранение состояния текущего выделенного объекта.

let hoveredFeature = null;

map.on('pointermove', function (event) {
  let found = null;

  map.forEachFeatureAtPixel(event.pixel, function (feature) {
    found = feature;
  });

  if (hoveredFeature !== found) {
    if (hoveredFeature) {
      hoveredFeature.setStyle(undefined);
    }

    if (found) {
      found.setStyle(new ol.style.Style({
        stroke: new ol.style.Stroke({
          color: 'red',
          width: 3
        }),
        fill: new ol.style.Fill({
          color: 'rgba(255,0,0,0.2)'
        })
      }));
    }

    hoveredFeature = found;
  }
});

Такой подход применим для небольших наборов данных. При больших объёмах он приводит к перегрузке из-за постоянного создания стилей.


Hover через состояние feature (property-based styling)

Более стабильный способ — использование свойства объекта и динамического стиля слоя.

const vectorLayer = new ol.layer.Vector({
  source: vectorSource,
  style: function (feature) {
    const hovered = feature.get('hovered');

    return new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: hovered ? '#ff0000' : '#3399CC',
        width: hovered ? 3 : 1
      }),
      fill: new ol.style.Fill({
        color: hovered ? 'rgba(255,0,0,0.3)' : 'rgba(51,153,204,0.1)'
      })
    });
  }
});

Обновление состояния:

map.on('pointermove', function (event) {
  vectorSource.forEachFeature(function (f) {
    if (f.get('hovered')) {
      f.set('hovered', false);
    }
  });

  map.forEachFeatureAtPixel(event.pixel, function (feature) {
    feature.set('hovered', true);
  });
});

Такой подход лучше масштабируется, так как стиль централизован.


Управление курсором при наведении

Изменение курсора — важный визуальный сигнал взаимодействия.

map.on('pointermove', function (event) {
  const hit = map.hasFeatureAtPixel(event.pixel);

  map.getTargetElement().style.cursor = hit ? 'pointer' : '';
});

Для более точного контроля используется forEachFeatureAtPixel, позволяющий учитывать только определённые слои.


Оптимизация hover-логики

Постоянное выполнение логики на pointermove создаёт нагрузку. Основные оптимизации:

Ограничение частоты обработки

let lastTime = 0;

map.on('pointermove', function (event) {
  const now = Date.now();
  if (now - lastTime < 16) return;
  lastTime = now;

  // обработка
});

Использование раннего выхода

map.on('pointermove', function (event) {
  let hit = false;

  map.forEachFeatureAtPixel(event.pixel, function () {
    hit = true;
    return true;
  });

  if (!hit) return;
});

Ограничение слоя

map.forEachFeatureAtPixel(event.pixel, function (feature, layer) {
  if (layer !== targetLayer) return;
});

Tooltip при наведении

Hover часто сопровождается всплывающей подсказкой через Overlay.

const tooltip = new ol.Overlay({
  element: document.getElementById('tooltip'),
  offset: [10, 0]
});

map.addOverlay(tooltip);

Логика отображения:

map.on('pointermove', function (event) {
  const feature = map.forEachFeatureAtPixel(event.pixel, f => f);

  if (feature) {
    const coord = event.coordinate;

    tooltip.setPosition(coord);
    tooltip.getElement().innerHTML = feature.get('name');
    tooltip.getElement().style.display = 'block';
  } else {
    tooltip.getElement().style.display = 'none';
  }
});

Hover через Select interaction

Встроенный механизм взаимодействия — Select. Он поддерживает активацию по наведению через условие.

const selectHover = new ol.interaction.Select({
  condition: ol.events.condition.pointerMove
});

map.addInteraction(selectHover);

Дальнейшая работа идёт через события selection:

selectHover.on('select', function (event) {
  console.log(event.selected);
});

Этот подход снижает необходимость ручного управления стилями.


Работа с несколькими слоями

При наличии нескольких vector-слоёв порядок важен. Hover определяется первым найденным объектом.

Фильтрация слоя:

map.forEachFeatureAtPixel(event.pixel, function (feature, layer) {
  if (layer.get('hoverEnabled')) {
    return feature;
  }
});

Приоритет слоёв может регулироваться через zIndex, но логика hover остаётся на уровне кода.


Hover в условиях кластеризации

При использовании кластеров (ol.source.Cluster) hover применяется к группам объектов.

map.forEachFeatureAtPixel(event.pixel, function (feature) {
  const features = feature.get('features');

  if (features && features.length > 1) {
    // кластер
  }
});

Поведение обычно разделяется:

  • одиночный объект — обычная подсветка
  • кластер — изменение размера или цвета круга

Поведение на границах и мелких объектах

Особенности геометрии приводят к неточностям попадания курсора. Основные решения:

  • увеличение hitTolerance
  • использование буферизации геометрий
  • переход на Circle для точек вместо пиксельной проверки
map.forEachFeatureAtPixel(event.pixel, function (feature) {
  const geometry = feature.getGeometry();
  const extent = geometry.getExtent();
});

Сброс состояния hover

Критический момент — корректное очищение состояния при уходе курсора с объекта.

map.on('pointermove', function (event) {
  if (event.dragging) return;

  const feature = map.forEachFeatureAtPixel(event.pixel, f => f);

  if (!feature && hoveredFeature) {
    hoveredFeature.set('hovered', false);
    hoveredFeature = null;
  }
});

Это предотвращает «залипание» подсветки при быстром перемещении курсора или зуме.