Интерфейс IControl

В Mapbox GL JS управление элементами интерфейса карты строится вокруг единого контрактного подхода. Любой пользовательский элемент управления реализует интерфейс IControl, который определяет жизненный цикл контролов и их интеграцию в карту.

Основная идея заключается в том, что карта не зависит от конкретной реализации UI-компонентов. Любой элемент — масштабирование, кнопка, переключатель слоя или кастомная панель — подключается через единый интерфейс.


Назначение IControl

IControl определяет поведение пользовательских контролов, которые добавляются в интерфейс карты. Через него обеспечивается:

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

Контролы не работают напрямую с внутренними слоями рендеринга Mapbox GL JS. Они взаимодействуют только через публичный API карты.


Структура интерфейса IControl

Интерфейс задаёт минимальный набор методов, необходимых для интеграции пользовательского элемента:

  • onAdd(map)
  • onRemove(map)

Дополнительно могут использоваться вспомогательные свойства, влияющие на позиционирование.


Метод onAdd

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

Сигнатура

onAdd(map: Map): HTMLElement

Назначение

  • создание DOM-элемента управления;
  • привязка логики к экземпляру карты;
  • инициализация событий;
  • возврат HTML-узла, который будет добавлен в интерфейс карты.

Особенности поведения

  • вызывается один раз при map.addControl(control);
  • должен возвращать корневой DOM-элемент;
  • доступ к API карты передаётся через параметр map.

Пример реализации

class ZoomInfoControl {
    onAdd(map) {
        this._map = map;

        this._container = document.createElement('div');
        this._container.className = 'mapboxgl-ctrl zoom-info';

        this._container.textContent = `Zoom: ${map.getZoom().toFixed(2)}`;

        this._map.on('zoom', this._update.bind(this));

        return this._container;
    }

    _update() {
        this._container.textContent = `Zoom: ${this._map.getZoom().toFixed(2)}`;
    }
}

Метод onRemove

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

Сигнатура

onRemove(map: Map): void

Назначение

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

Пример реализации

class ZoomInfoControl {
    onRemove() {
        this._map.off('zoom', this._update.bind(this));
        this._container.parentNode.removeChild(this._container);
        this._map = undefined;
    }
}

Контейнер контрола

DOM-элемент, возвращаемый из onAdd, автоматически помещается в стандартный контейнер Mapbox GL JS.

Контейнер:

  • имеет фиксированное позиционирование;
  • располагается в одном из углов карты;
  • наследует стили Mapbox;
  • поддерживает группировку нескольких контролов.

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

Mapbox GL JS поддерживает четыре стандартные позиции:

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

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

map.addControl(new ZoomInfoControl(), 'top-right');

Если позиция не задана, используется значение по умолчанию, определяемое самим контролом.


Свойство getDefaultPosition

Некоторые реализации контролов определяют метод getDefaultPosition, который задаёт стандартное расположение при отсутствии явного указания.

Сигнатура

getDefaultPosition(): string

Возможные значения

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

Пример

class CompassControl {
    getDefaultPosition() {
        return 'top-right';
    }
}

Полноценная реализация кастомного контрола

Контрол может объединять все элементы интерфейса IControl.

class LayerToggleControl {
    constructor(layers) {
        this._layers = layers;
    }

    onAdd(map) {
        this._map = map;

        this._container = document.createElement('div');
        this._container.className = 'mapboxgl-ctrl layer-toggle';

        this._layers.forEach(layer => {
            const button = document.createElement('button');
            button.textContent = layer.name;

            button.addEventListener('click', () => {
                const visibility = map.getLayoutProperty(layer.id, 'visibility');
                map.setLayoutProperty(
                    layer.id,
                    'visibility',
                    visibility === 'visible' ? 'none' : 'visible'
                );
            });

            this._container.appendChild(button);
        });

        return this._container;
    }

    onRemove() {
        this._container.innerHTML = '';
        this._map = undefined;
    }

    getDefaultPosition() {
        return 'top-left';
    }
}

Взаимодействие с жизненным циклом карты

Контролы, реализующие IControl, участвуют в жизненном цикле карты косвенно:

  • создаются после инициализации карты;
  • привязываются через map.addControl;
  • удаляются через map.removeControl;
  • реагируют на события карты через подписки.

Удаление контрола должно сопровождаться полной очисткой всех привязанных обработчиков, поскольку Mapbox GL JS не управляет внутренними зависимостями пользовательских компонентов.


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

Контейнеры контролов используют стандартные CSS-классы Mapbox:

  • mapboxgl-ctrl
  • mapboxgl-ctrl-group

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

.mapboxgl-ctrl.layer-toggle {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.mapboxgl-ctrl.layer-toggle button {
    background: white;
    border: none;
    padding: 6px 10px;
    cursor: pointer;
}

Стили не управляются библиотекой и полностью определяются разработчиком.


Типизация интерфейса

В TypeScript интерфейс описывается следующим образом:

interface IControl {
    onAdd(map: Map): HTMLElement;
    onRemove(map: Map): void;
    getDefaultPosition?(): string;
}

Типизация обеспечивает корректную интеграцию пользовательских контролов в систему Mapbox GL JS и предотвращает ошибки при реализации обязательных методов.


Поведение при множественных контролах

Несколько контролов могут быть добавлены в один контейнер позиции. В этом случае:

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

Ограничения модели IControl

Архитектура интерфейса накладывает ряд ограничений:

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

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