Разработка кастомных взаимодействий

Система взаимодействий (Interactions) в OpenLayers построена как расширяемый слой обработки пользовательского ввода, отделённый от логики отображения слоёв и источников данных. Каждый interaction представляет собой модуль, который подключается к карте и перехватывает события мыши и клавиатуры, преобразуя их в действия над картой или геометрией.

Ключевая идея архитектуры заключается в том, что карта не содержит «жёстко встроенного поведения» — масштабирование, перемещение, выделение объектов реализованы через набор независимых взаимодействий, которые можно комбинировать, отключать или заменять.

Основные особенности:

  • взаимодействия подключаются к экземпляру карты через коллекцию ol.interaction
  • каждое взаимодействие может слушать события указателя (pointer events)
  • порядок обработки определяется стеком взаимодействий
  • взаимодействия могут блокировать дальнейшее распространение события

Базовая модель Interaction

Любое взаимодействие в OpenLayers наследуется от базового класса ol/interaction/Interaction.

Типовая структура кастомного взаимодействия:

import Interaction from 'ol/interaction/Interaction';

class CustomInteraction extends Interaction {
  constructor(options = {}) {
    super({
      handleDownEvent: options.handleDownEvent,
      handleDragEvent: options.handleDragEvent,
      handleMoveEvent: options.handleMoveEvent,
      handleUpEvent: options.handleUpEvent,
    });
  }
}

Внутренне Interaction работает через набор обработчиков событий:

  • handleDownEvent — нажатие кнопки мыши
  • handleDragEvent — перемещение при зажатой кнопке
  • handleMoveEvent — движение курсора
  • handleUpEvent — отпускание кнопки
  • handleClick — одиночный клик (опционально)

Каждый из этих методов может возвращать true или false, управляя тем, будет ли событие передано следующим взаимодействиям.


Регистрация взаимодействия в карте

Добавление кастомного поведения выполняется через коллекцию interactions карты:

import Map from 'ol/Map';
import View from 'ol/View';

const map = new Map({
  target: 'map',
  view: new View({
    center: [0, 0],
    zoom: 2,
  }),
});

map.addInteraction(new CustomInteraction());

Порядок добавления имеет значение: взаимодействия обрабатываются последовательно, и некоторые могут блокировать события.


Работа с событиями указателя

В основе кастомных взаимодействий лежит работа с MapBrowserEvent. Этот объект содержит:

  • координаты курсора в пикселях
  • координаты в проекции карты
  • оригинальное DOM-событие
  • информацию о пикселе под курсором

Пример использования:

handleMoveEvent(evt) {
  const coordinate = evt.coordinate;
  const pixel = evt.pixel;

  console.log('Координаты:', coordinate);
  console.log('Пиксель:', pixel);
}

Ключевой момент — разделение экранных координат и географических координат. Это позволяет строить взаимодействия, не зависящие от текущего масштаба карты.


Управление состоянием взаимодействия

Interaction часто реализует внутреннюю конечную машину состояний:

  • idle (ожидание)
  • dragging (перетаскивание)
  • active (активное состояние)

Пример:

class DragBoxInteraction extends Interaction {
  constructor() {
    super();
    this.dragging = false;
    this.startPixel = null;
  }

  handleDownEvent(evt) {
    this.dragging = true;
    this.startPixel = evt.pixel;
    return true;
  }

  handleDragEvent(evt) {
    if (!this.dragging) return;

    const current = evt.pixel;
    console.log('drag from', this.startPixel, 'to', current);
  }

  handleUpEvent() {
    this.dragging = false;
    this.startPixel = null;
    return false;
  }
}

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


Использование condition-функций

OpenLayers широко использует функции условий (condition functions) для управления тем, когда interaction активен.

Примеры стандартных условий:

  • singleClick
  • doubleClick
  • shiftKeyOnly
  • altKeyOnly

Пример кастомной логики:

import {shiftKeyOnly} from 'ol/events/condition';

class ShiftDragInteraction extends Interaction {
  handleDownEvent(evt) {
    if (!shiftKeyOnly(evt)) {
      return false;
    }
    this.active = true;
    this.start = evt.coordinate;
    return true;
  }
}

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


Создание взаимодействия для выделения объектов

Одним из типичных сценариев является выделение feature под курсором.

import Sel ect from 'ol/interaction/Select';

const select = new Select({
  condition: function (evt) {
    return evt.type === 'pointermove';
  },
});

Однако кастомная реализация даёт больше контроля:

import {getFeaturesAtPixel} fr om 'ol/render/canvas';

class HoverSelectInteraction extends Interaction {
  handleMoveEvent(evt) {
    const map = evt.map;

    map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
      feature.set('hover', true);
    });
  }
}

Важный аспект — производительность. Постоянные запросы к forEachFeatureAtPixel требуют оптимизации на больших слоях.


Drag-поведение и геометрические трансформации

Перетаскивание объектов — один из самых сложных типов взаимодействий.

Типовой алгоритм:

  1. определить feature под курсором
  2. сохранить начальную позицию
  3. вычислить смещение при движении
  4. применить трансформацию координат
class MoveFeatureInteraction extends Interaction {
  constructor() {
    super();
    this.feature = null;
    this.startCoord = null;
  }

  handleDownEvent(evt) {
    this.feature = evt.map.forEachFeatureAtPixel(
      evt.pixel,
      f => f
    );

    if (this.feature) {
      this.startCoord = evt.coordinate;
      return true;
    }

    return false;
  }

  handleDragEvent(evt) {
    if (!this.feature) return;

    const dx = evt.coordinate[0] - this.startCoord[0];
    const dy = evt.coordinate[1] - this.startCoord[1];

    const geometry = this.feature.getGeometry();
    geometry.translate(dx, dy);

    this.startCoord = evt.coordinate;
  }

  handleUpEvent() {
    this.feature = null;
    return false;
  }
}

Работа с курсором и визуальной обратной связью

Interactions часто изменяют стиль курсора через DOM-элемент map viewport:

handleMoveEvent(evt) {
  const element = evt.map.getTargetElement();

  const hit = evt.map.hasFeatureAtPixel(evt.pixel);

  element.style.cursor = hit ? 'pointer' : 'default';
}

Дополнительные состояния могут отображаться через overlay или изменение стилей feature.


Перехват и блокировка событий

Interaction может остановить дальнейшую обработку события:

handleDownEvent(evt) {
  evt.originalEvent.preventDefault();
  return true;
}

Возврат true означает, что событие «поглощено» текущим interaction и не передаётся следующим обработчикам.

Это важно при построении:

  • кастомных инструментов рисования
  • систем выделения
  • редакторов геометрии

Интеграция с изменением геометрий

Кастомные interactions часто взаимодействуют с ol/geom:

  • Point
  • LineString
  • Polygon

Пример модификации линии:

handleDragEvent(evt) {
  const geometry = this.feature.getGeometry();

  const coordinates = geometry.getCoordinates();
  coordinates.push(evt.coordinate);

  geometry.setCoordinates(coordinates);
}

При этом важно учитывать immutability подходы: в некоторых случаях лучше создавать новый массив координат вместо модификации существующего.


Производительность кастомных interactions

Критические аспекты:

  • минимизация операций внутри handleMoveEvent
  • избегание тяжёлых вычислений на каждый пиксель движения
  • кэширование результатов forEachFeatureAtPixel
  • ограничение частоты обновлений через throttling

Пример оптимизации:

handleMoveEvent(evt) {
  if (!this._lastTime || Date.now() - this._lastTime > 16) {
    this._lastTime = Date.now();
    this.process(evt);
  }
}

Комбинирование взаимодействий

OpenLayers позволяет одновременно использовать несколько interactions:

  • DragPan
  • MouseWheelZoom
  • кастомные инструменты редактирования

Порядок имеет значение:

map.addInteraction(new CustomInteraction());
map.addInteraction(new Select());
map.addInteraction(new DragPan());

В этом случае кастомный interaction может перехватывать события раньше стандартных.


Расширение через наследование и композицию

Подходы к созданию сложных инструментов:

Наследование

  • расширение базового Interaction
  • переопределение методов событий

Композиция

  • объединение нескольких простых interactions
  • координация через общий state manager

Пример композиции:

class DrawingTool {
  constructor(map) {
    this.map = map;
    this.draw = new DrawInteraction();
    this.modify = new ModifyInteraction();

    map.addInteraction(this.draw);
    map.addInteraction(this.modify);
  }
}

Работа с pointer events и touch-устройствами

OpenLayers абстрагирует различия между:

  • mouse events
  • touch events
  • pen input

Все они приводятся к единому MapBrowserEventPointer.

Это позволяет кастомным interactions работать одинаково на:

  • десктопе
  • планшетах
  • мобильных устройствах

Особое внимание уделяется:

  • multi-touch gestures
  • pinch zoom
  • inertia scrolling

Контроль взаимодействий через activation

Interaction может быть включён или выключен динамически:

interaction.setActive(false);
interaction.setActive(true);

Это используется для:

  • переключения режимов редактора
  • временного отключения обработки
  • конфликтующих инструментов

Связь с состоянием карты

Interactions тесно связаны с состоянием Map:

  • view transformation
  • resolution
  • center changes

Пример использования view в interaction:

handleDragEvent(evt) {
  const view = evt.map.getView();
  const resolution = view.getResolution();

  console.log('resolution:', resolution);
}

Это позволяет адаптировать поведение к масштабу карты, например изменять чувствительность перетаскивания.