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

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

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

Ключевые особенности системы контролов:

  • независимость от слоёв карты;
  • возможность размещения в фиксированных позициях интерфейса;
  • реактивная привязка к объекту карты;
  • управление через DOM и события браузера.

Базовый класс Control

Все пользовательские контролы наследуются от ol/control/Control. Этот класс принимает объект конфигурации, содержащий DOM-элемент и функции жизненного цикла.

Минимальная структура кастомного контрола:

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

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

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

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

    button.addEventListener('click', this.handleClick.bind(this));
  }

  handleClick() {
    console.log('Контрол активирован');
  }
}

Здесь ключевым моментом является передача element в super(). OpenLayers использует его для вставки в DOM карты.


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

Контрол не существует сам по себе — он всегда связан с экземпляром карты. Добавление происходит через метод addControl.

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

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

const control = new MyControl();
map.addControl(control);

После добавления OpenLayers автоматически размещает контрол в одном из контейнеров интерфейса.


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

Контролы размещаются в фиксированных зонах интерфейса карты. За это отвечает свойство target и CSS-классы позиционирования.

Стандартные зоны:

  • ol-control-top-left
  • ol-control-top-right
  • ol-control-bottom-left
  • ol-control-bottom-right

Пример задания позиции:

import { defaults as defaultControls } from 'ol/control.js';

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

Внутри CSS можно управлять расположением:

.my-control {
  position: absolute;
  top: 10px;
  left: 10px;
}

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

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

  1. Создание DOM-элемента
  2. Привязка к карте
  3. Вставка в контейнер управления
  4. Реакция на изменения состояния карты
  5. Уничтожение (при удалении карты или контрола)

Метод setMap можно переопределить для реакции на подключение:

setMap(map) {
  super.setMap(map);
  if (map) {
    console.log('Контрол добавлен на карту');
  }
}

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

Контрол может взаимодействовать с картой через объект map. Это позволяет изменять центр, масштаб, слои и другие параметры.

Пример управления масштабом:

handleClick() {
  const map = this.getMap();
  if (!map) return;

  const view = map.getView();
  const zoom = view.getZoom();

  view.setZoom(zoom + 1);
}

Такой подход делает контрол фактически инструментом управления состоянием карты.


Создание сложного DOM-контрола

Контролы могут содержать сложные интерфейсы: формы, переключатели, списки.

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

class SearchControl extends Control {
  constructor() {
    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'Поиск';

    const button = document.createElement('button');
    button.textContent = 'Найти';

    const container = document.createElement('div');
    container.className = 'search-control ol-control';
    container.appendChild(input);
    container.appendChild(button);

    super({ element: container });

    button.addEventListener('click', () => {
      this.search(input.value);
    });
  }

  search(query) {
    const map = this.getMap();
    if (!map) return;

    console.log('Поиск:', query);
  }
}

Управление событиями внутри контрола

Контролы часто используют события DOM и события карты одновременно. Важно предотвращать всплытие событий, чтобы взаимодействие с интерфейсом не влияло на карту.

element.addEventListener('click', (e) => {
  e.stopPropagation();
});

Для интеграции с картой используются события OpenLayers:

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

Стилизация контролов

Контролы требуют явного CSS для корректного отображения. OpenLayers предоставляет базовые классы, но внешний вид задаётся разработчиком.

Пример базового оформления:

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

.my-control button {
  background: transparent;
  border: none;
  cursor: pointer;
}

Класс ol-unselectable предотвращает выделение текста внутри контрола, что важно для UX.


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

Контрол может хранить внутреннее состояние, например активен/неактивен.

class ToggleControl extends Control {
  constructor() {
    const button = document.createElement('button');
    button.textContent = 'OFF';

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

    super({ element });

    this.active = false;

    button.addEventListener('click', () => {
      this.active = !this.active;
      button.textContent = this.active ? 'ON' : 'OFF';
    });
  }
}

Такой подход позволяет строить переключатели слоёв, режимов взаимодействия и фильтров.


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

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

toggleLayer() {
  const map = this.getMap();
  const layer = map.getLayers().item(0);

  layer.setVisible(!layer.getVisible());
}

Расширенные контролы могут работать с коллекцией слоёв и динамически строить интерфейс.


Пользовательский контрол масштабирования

Реализация собственного zoom-контрола демонстрирует базовые принципы управления картой.

class ZoomControl extends Control {
  constructor() {
    const zoomIn = document.createElement('button');
    zoomIn.textContent = '+';

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

    const container = document.createElement('div');
    container.className = 'zoom-control ol-control';
    container.appendChild(zoomIn);
    container.appendChild(zoomOut);

    super({ element: container });

    zoomIn.addEventListener('click', () => this.changeZoom(1));
    zoomOut.addEventListener('click', () => this.changeZoom(-1));
  }

  changeZoom(delta) {
    const map = this.getMap();
    if (!map) return;

    const view = map.getView();
    view.setZoom(view.getZoom() + delta);
  }
}

Обработка синхронизации состояния

При сложных интерфейсах контрол должен реагировать на изменения карты извне. Например, обновлять отображение текущего масштаба.

class ZoomDisplayControl extends Control {
  constructor() {
    const element = document.createElement('div');
    element.className = 'zoom-display ol-control';

    super({ element });

    this.element = element;
  }

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

    if (map) {
      map.getView().on('change:resolution', () => {
        const zoom = map.getView().getZoom();
        this.element.textContent = `Zoom: ${zoom}`;
      });
    }
  }
}

Пользовательские события и расширение логики

Контрол может генерировать собственные события для взаимодействия с внешним кодом.

class EventControl extends Control {
  constructor() {
    const button = document.createElement('button');
    button.textContent = 'Emit';

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

    super({ element });

    button.addEventListener('click', () => {
      this.dispatchEvent('custom:event');
    });
  }
}

Подписка на событие:

const control = new EventControl();

control.on('custom:event', () => {
  console.log('Событие контрола');
});

Архитектурные принципы проектирования контролов

При разработке сложных контролов важно учитывать несколько принципов:

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

Контролы становятся устойчивыми компонентами интерфейса только при строгом разделении логики и представления.


Расширение стандартных контролов

OpenLayers предоставляет готовые контролы, такие как масштабирование, атрибуция, поворот. Их можно расширять, переопределяя поведение или внешний вид.

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

class CustomZoom extends Zoom {
  constructor(options) {
    super(options);
    this.element.classList.add('custom-zoom');
  }
}

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