Расширение класса Control

В библиотеке Leaflet элементы управления (controls) представляют собой специализированные UI-компоненты, интегрированные в карту и размещаемые в фиксированных позициях поверх тайлового слоя. Все стандартные элементы, такие как масштабирование или переключение слоёв, построены на основе общего абстрактного класса L.Control.

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


Класс L.Control и его роль в системе Leaflet

L.Control является базовым строительным блоком для всех пользовательских элементов интерфейса. Он отвечает за:

  • добавление элемента в DOM карты;
  • управление позиционированием;
  • подключение и отключение обработчиков событий;
  • жизненный цикл UI-компонента.

Основной принцип: контрол не управляет картой напрямую, он лишь взаимодействует с ней через API Leaflet.


Жизненный цикл пользовательского контроля

При расширении L.Control ключевыми методами являются:

onAdd(map)

Метод вызывается при добавлении контроля на карту.

Обязанности:

  • создание DOM-элемента;
  • привязка обработчиков событий;
  • возврат корневого элемента.
L.Control.MyControl = L.Control.extend({
  onAdd: function (map) {
    const container = L.DomUtil.create('div', 'leaflet-bar custom-control');

    container.innerHTML = 'Кнопка';

    return container;
  }
});

onRemove(map)

Вызывается при удалении контроля с карты.

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

  • очистки обработчиков событий;
  • освобождения ссылок;
  • предотвращения утечек памяти.
onRemove: function (map) {
  // очистка ресурсов
}

Создание собственного класса контроля

Базовая структура расширения L.Control строится через L.Control.extend.

L.Control.ButtonControl = L.Control.extend({
  options: {
    position: 'topright'
  },

  onAdd: function (map) {
    const container = L.DomUtil.create('div', 'leaflet-bar custom-button');

    container.innerHTML = 'OK';

    L.DomEvent.on(container, 'click', this._onClick, this);

    this._map = map;

    return container;
  },

  onRemove: function () {
    // освобождение обработчиков
  },

  _onClick: function () {
    alert('Нажато');
  }
});

Регистрация контроля через фабричную функцию

Leaflet использует паттерн фабрики для удобного создания экземпляров:

L.control.buttonControl = function (opts) {
  return new L.Control.ButtonControl(opts);
};

Использование:

L.control.buttonControl({ position: 'topleft' }).addTo(map);

Управление позиционированием

Leaflet поддерживает четыре базовые позиции:

  • topleft
  • topright
  • bottomleft
  • bottomright

При расширении класса позиция задаётся через options.

options: {
  position: 'bottomleft'
}

Внутренне Leaflet группирует контролы по контейнерам, создавая отдельные DOM-блоки для каждой позиции.


Работа с DOM внутри контролов

Leaflet предоставляет утилиты L.DomUtil и L.DomEvent для безопасного взаимодействия с DOM.

Создание элементов

const container = L.DomUtil.create('div', 'custom-control');

Отключение всплытия событий

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

L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);

Это предотвращает:

  • случайное перемещение карты при клике;
  • зум при прокрутке внутри панели.

Подключение к экземпляру карты

Контрол получает доступ к карте через параметр map в onAdd.

onAdd: function (map) {
  this._map = map;
}

Через него можно:

  • менять центр карты;
  • добавлять маркеры;
  • реагировать на события карты.
this._map.setView([51.505, -0.09], 13);

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

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

Пример: отображение текущего масштаба

L.Control.ZoomDisplay = L.Control.extend({
  onAdd: function (map) {
    this._container = L.DomUtil.create('div', 'zoom-display');

    this._container.innerHTML = map.getZoom();

    map.on('zoomend', this._update, this);

    return this._container;
  },

  onRemove: function (map) {
    map.off('zoomend', this._update, this);
  },

  _update: function () {
    this._container.innerHTML = this._map.getZoom();
  }
});

Ключевой принцип: контрол подписывается на события карты и синхронизирует UI.


Контрол с состоянием (toggle button)

Частый сценарий — переключение режима.

L.Control.Toggle = L.Control.extend({
  options: {
    position: 'topright'
  },

  onAdd: function (map) {
    this._active = false;

    const container = L.DomUtil.create('div', 'leaflet-bar toggle-control');

    this._button = L.DomUtil.create('a', '', container);
    this._button.href = '#';
    this._button.innerHTML = 'OFF';

    L.DomEvent.on(this._button, 'click', this._toggle, this);

    return container;
  },

  _toggle: function (e) {
    L.DomEvent.preventDefault(e);

    this._active = !this._active;

    this._button.innerHTML = this._active ? 'ON' : 'OFF';
  }
});

Интеграция с API карты

Контролы часто служат мостом между UI и логикой карты.

Пример: включение режима рисования или добавления маркеров.

_onClick: function () {
  this._map.on('click', this._addMarker, this);
},

_addMarker: function (e) {
  L.marker(e.latlng).addTo(this._map);
}

Важно контролировать жизненный цикл обработчиков, чтобы не оставлять активные подписки.


Динамическое обновление интерфейса

Контрол может изменять DOM в зависимости от состояния приложения.

setEnabled: function (enabled) {
  if (enabled) {
    this._container.classList.add('active');
  } else {
    this._container.classList.remove('active');
  }
}

Такой подход используется для:

  • индикаторов активности;
  • переключателей слоёв;
  • режимов редактирования.

Контролы с вложенной структурой

Более сложные интерфейсы могут содержать несколько элементов:

onAdd: function () {
  const container = L.DomUtil.create('div', 'complex-control');

  const btn1 = L.DomUtil.create('button', '', container);
  btn1.innerHTML = 'A';

  const btn2 = L.DomUtil.create('button', '', container);
  btn2.innerHTML = 'B';

  L.DomEvent.on(btn1, 'click', this._actionA, this);
  L.DomEvent.on(btn2, 'click', this._actionB, this);

  return container;
}

Использование CSS в пользовательских контролах

Leaflet не ограничивает стилизацию. Контролы полностью управляются через CSS-классы.

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

Стили должны учитывать:

  • фиксированное позиционирование;
  • перекрытие карты;
  • кликабельность элементов.

Управление доступностью DOM-элементов карты

Leaflet автоматически добавляет контролы в контейнер .leaflet-control-container. Важно учитывать:

  • z-index относительно тайлов;
  • предотвращение конфликтов с другими слоями;
  • изоляцию событий.

Переиспользуемые контролы и модульная архитектура

Контролы удобно оформлять как независимые модули:

export const MyControl = L.Control.extend({ ... });

export function myControl(options) {
  return new MyControl(options);
}

Такой подход обеспечивает:

  • повторное использование;
  • тестируемость;
  • интеграцию с сборщиками (Webpack, Vite).

Взаимодействие нескольких контролов

Контролы могут обмениваться состоянием через карту:

map.myState = { mode: 'edit' };

Или через события:

map.fire('modechange', { mode: 'edit' });
map.on('modechange', handler);

Это позволяет строить сложные интерфейсы без жёсткой связности компонентов.


Типичные ошибки при расширении L.Control

  • отсутствие удаления обработчиков в onRemove;
  • прямое изменение DOM карты вне контейнера контроля;
  • пропуск L.DomEvent.disableClickPropagation;
  • хранение состояния вне экземпляра класса;
  • привязка логики к глобальным переменным.

Расширенные паттерны построения контролов

Контрол как мини-компонент

Контрол может включать:

  • собственное состояние;
  • внутренние методы;
  • реакцию на события карты;
  • синхронизацию с внешними сервисами.

Контрол как сервисный слой UI

Некоторые контролы выполняют роль интерфейса к данным:

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

Контролы и асинхронные данные

Контрол может работать с API:

fetch('/api/data')
  .then(r => r.json())
  .then(data => {
    this._render(data);
  });

Важно учитывать:

  • обновление UI после загрузки;
  • обработку ошибок;
  • блокировку повторных запросов.

Масштабирование архитектуры контролов

При росте приложения контролы становятся частью архитектурного слоя:

  • UI-контролы (кнопки, панели);
  • функциональные контролы (рисование, фильтры);
  • информационные контролы (легенды, статистика).

Разделение по ответственности упрощает поддержку и расширение системы.