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

Контролы в OpenLayers представляют собой самостоятельные UI-элементы, встроенные в контейнер карты и связанные с её жизненным циклом. Базовая точка расширения — класс ol/control/Control, который инкапсулирует DOM-узел, позиционирование и привязку к экземпляру карты.

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


Базовая структура кастомного контрола

Любой пользовательский контрол строится вокруг наследования или композиции Control. Основные элементы:

  • DOM-элемент (element)
  • обработчики событий
  • привязка к карте через setMap
  • логика обновления состояния

Простейшая реализация:

import Control from 'ol/control/Control.js';

class SimpleButtonControl extends Control {
  constructor(options = {}) {
    const button = document.createElement('button');
    button.innerHTML = 'Click';

    const element = document.createElement('div');
    element.className = 'custom-control ol-unselectable ol-control';
    element.appendChild(button);

    super({
      element: element,
      target: options.target
    });

    button.addEventListener('click', () => {
      console.log('Control activated');
    });
  }
}

Ключевой момент: Control ожидает готовый DOM-элемент, который он помещает в контейнер карты.


Жизненный цикл контрола

Контрол проходит несколько стадий:

1. Создание

DOM формируется до подключения к карте.

2. Привязка к карте (setMap)

Метод вызывается автоматически при добавлении в map.addControl().

setMap(map) {
  super.setMap(map);
}

В этот момент можно подписываться на события карты:

this.getMap().on('moveend', () => {
  console.log('Карта перемещена');
});

Добавление контрола на карту

Контрол регистрируется через коллекцию controls:

import Map from 'ol/Map.js';

const map = new Map({
  target: 'map',
  controls: []
});

map.addControl(new SimpleButtonControl());

Контрол автоматически вставляется в контейнер ol-overlaycontainer-stopevent.


Работа с состоянием карты

Контролы часто отражают состояние карты: масштаб, центр, слой.

Пример контрола отображения координат курсора:

import Control from 'ol/control/Control.js';

class MousePositionControl extends Control {
  constructor() {
    const element = document.createElement('div');
    element.className = 'mouse-position ol-control';

    super({ element });

    this.element = element;
  }

  setMap(map) {
    super.setMap(map);

    if (!map) return;

    map.on('pointermove', (event) => {
      const coord = event.coordinate;
      this.element.innerHTML = coord
        .map(c => c.toFixed(2))
        .join(', ');
    });
  }
}

Управление DOM и стилями

Контролы полностью зависят от CSS. OpenLayers использует классы:

  • ol-control
  • ol-unselectable
  • ol-zoom
  • ol-rotate

Кастомный стиль:

.custom-control {
  background: white;
  padding: 6px;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}

.custom-control button {
  border: none;
  background: #2a7ae2;
  color: white;
  padding: 4px 8px;
  cursor: pointer;
}

Важно учитывать, что все контролы находятся поверх карты и должны поддерживать pointer-events корректно.


Контролы с изменяемым состоянием

Контрол может хранить внутреннее состояние и синхронизироваться с картой.

Пример переключателя слоя:

class LayerToggleControl extends Control {
  constructor(layer) {
    const button = document.createElement('button');
    button.textContent = 'Toggle';

    const element = document.createElement('div');
    element.className = 'custom-control';
    element.appendChild(button);

    super({ element });

    this.layer = layer;
    this.visible = true;

    button.addEventListener('click', () => {
      this.visible = !this.visible;
      this.layer.setVisible(this.visible);
    });
  }
}

Использование событий карты

Контролы часто зависят от событий:

  • moveend
  • pointermove
  • change:resolution
  • change:visible

Пример масштаб-зависимого отображения:

setMap(map) {
  super.setMap(map);

  const view = map.getView();

  const update = () => {
    const zoom = view.getZoom();
    this.element.innerHTML = `Zoom: ${zoom.toFixed(2)}`;
  };

  view.on('change:resolution', update);
  update();
}

Позиционирование контролов

OpenLayers поддерживает стандартные позиции:

  • top-left
  • top-right
  • bottom-left
  • bottom-right

Задание позиции:

import Control from 'ol/control/Control.js';

const control = new Control({
  element: document.createElement('div'),
  target: undefined
});

// CSS класс позиции задаётся контейнером
control.element.className += ' ol-control ol-top-right';

Чаще позиционирование управляется через встроенные контролы или CSS контейнеры.


Композиция сложных контролов

Контрол может содержать несколько элементов:

class ToolbarControl extends Control {
  constructor() {
    const element = document.createElement('div');
    element.className = 'toolbar';

    const zoomIn = document.createElement('button');
    zoomIn.textContent = '+';

    const zoomOut = document.createElement('button');
    zoomOut.textContent = '-';

    element.appendChild(zoomIn);
    element.appendChild(zoomOut);

    super({ element });

    zoomIn.addEventListener('click', () => {
      const view = this.getMap().getView();
      view.setZoom(view.getZoom() + 1);
    });

    zoomOut.addEventListener('click', () => {
      const view = this.getMap().getView();
      view.setZoom(view.getZoom() - 1);
    });
  }
}

Очистка ресурсов и уничтожение

При удалении карты или контрола важно отписываться от событий:

setMap(map) {
  if (this._listener && this.getMap()) {
    this.getMap().un('moveend', this._listener);
  }

  super.setMap(map);

  if (map) {
    this._listener = () => console.log('moveend');
    map.on('moveend', this._listener);
  }
}

Без корректной очистки возможно накопление обработчиков и утечки памяти.


Интеграция с реактивным состоянием

Контрол может синхронизироваться с внешним состоянием приложения:

  • Redux / Zustand / MobX
  • собственные EventEmitter
  • WebSocket-данные

Пример подписки на внешний источник:

class ExternalStateControl extends Control {
  constructor(store) {
    const element = document.createElement('div');
    element.className = 'state-control';

    super({ element });

    this.store = store;

    this.store.subscribe((state) => {
      this.element.innerHTML = state.label;
    });
  }
}

Поведение при изменении размера карты

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

window.addEventListener('resize', () => {
  const map = this.getMap();
  if (map) {
    map.updateSize();
  }
});

Типовые ошибки при разработке контролов

  • создание DOM вне конструктора и потеря ссылки
  • отсутствие отписки от событий карты
  • использование глобальных переменных вместо getMap()
  • прямое изменение состояния карты без учёта View
  • конфликт CSS классов с встроенными стилями OpenLayers

Расширенные сценарии использования

Контролы часто применяются для:

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

Каждый из этих сценариев требует разделения логики: UI внутри контрола, бизнес-логика вне его, взаимодействие через события или состояние приложения.