Пользовательские контролы

Контролы в Leaflet представляют собой расширяемый механизм интерфейсных элементов, закреплённых в пределах карты и интегрированных в её жизненный цикл. Архитектура контролов построена вокруг базового класса L.Control, который обеспечивает единый контракт для добавления, удаления и управления DOM-элементами, привязанными к экземпляру карты.

Каждый контрол в Leaflet — это объект, наследующийся от L.Control. Его основная задача заключается в создании DOM-узла и его привязке к карте через метод addTo(map). Контрол не работает изолированно: он связан с экземпляром L.Map и реагирует на его состояние.

Ключевая особенность заключается в том, что контролы не рисуются внутри canvas или SVG-слоя карты. Они размещаются поверх карты в отдельной DOM-иерархии:

  • .leaflet-control-container

    • .leaflet-top
    • .leaflet-bottom
    • .leaflet-left
    • .leaflet-right

Каждый контрол позиционируется в одной из четырёх зон, задаваемых опцией position.

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

Создание пользовательского контрола осуществляется через расширение L.Control.extend. Базовая структура включает реализацию метода onAdd, который отвечает за создание DOM-элемента, и опционально onRemove, который вызывается при удалении контрола с карты.

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

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

        container.innerHTML = 'Кнопка';
        L.DomEvent.disableClickPropagation(container);

        container.oncl ick = function () {
            console.log('Контрол активирован');
        };

        return container;
    },

    onRemove: function (map) {
        // очистка ресурсов, если требуется
    }
});

L.control.myControl = function (opts) {
    return new L.Control.MyControl(opts);
};

После определения контрол добавляется на карту через:

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

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

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

  1. Инициализация — создание объекта через L.Control.extend.
  2. Добавление на карту — вызов addTo(map).
  3. Вызов onAdd(map) — создание DOM-структуры.
  4. Рендер в контейнере контролов — вставка в соответствующую позицию.
  5. Удаление — вызов remove() и затем onRemove(map).

Важный момент: DOM-элемент должен быть полностью автономным и не зависеть от внешнего состояния, кроме объекта карты.

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

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

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

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

Второй аргумент добавляет CSS-класс, который участвует в стилизации.

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

L.DomEvent.on(container, 'click', function (e) {
    console.log('click');
});

Блокировка событий карты

Для предотвращения взаимодействия кликов с картой используется:

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

Без этого контролы могут вызывать нежелательные перемещения карты при взаимодействии.

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

Опция position определяет размещение контрола:

  • topleft
  • topright
  • bottomleft
  • bottomright

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

Пример:

L.control.zoom({ position: 'bottomright' }).addTo(map);

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

Встроенный классический контрол

Leaflet предоставляет базовый пример контролов — масштабирование карты:

L.control.zoom({
    zoomInText: '+',
    zoomOutText: '-'
}).addTo(map);

Он реализован тем же механизмом L.Control, что и пользовательские расширения.

Кастомный контрол-кнопка

Часто используемый паттерн — одиночная кнопка с состоянием.

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

    onAdd: function (map) {
        const btn = L.DomUtil.create('button', 'leaflet-bar toggle-btn');
        btn.innerHTML = 'Слой';

        let active = false;

        L.DomEvent.on(btn, 'click', function () {
            active = !active;

            if (active) {
                btn.classList.add('active');
            } else {
                btn.classList.remove('active');
            }
        });

        L.DomEvent.disableClickPropagation(btn);

        return btn;
    }
});

Такая реализация сохраняет состояние внутри замыкания, не требуя глобального хранения.

Контрол с доступом к карте

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

L.Control.CenterButton = L.Control.extend({
    options: { position: 'topleft' },

    onAdd: function (map) {
        const container = L.DomUtil.create('div', 'leaflet-bar');
        container.innerHTML = 'Центр';

        L.DomEvent.on(container, 'click', function () {
            map.setView([51.505, -0.09], 13);
        });

        L.DomEvent.disableClickPropagation(container);

        return container;
    }
});

Контрол получает ссылку на map через аргумент onAdd, что делает возможным полный доступ к API карты.

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

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

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

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

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

        const base1 = L.tileLayer('url1');
        const base2 = L.tileLayer('url2');

        btn1.oncl ick = () => {
            map.addLayer(base1);
            map.removeLayer(base2);
        };

        btn2.oncl ick = () => {
            map.addLayer(base2);
            map.removeLayer(base1);
        };

        L.DomEvent.disableClickPropagation(container);

        return container;
    }
});

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

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

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

  • .leaflet-control — базовый стиль
  • .leaflet-bar — рамка с кнопками
  • .leaflet-control-container — контейнер всех контролов

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

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

Контролы не должны полагаться на глобальные стили страницы, иначе возможны конфликты отображения.

Управление состоянием и памятью

При работе с динамическими контролами важно учитывать освобождение ресурсов:

  • удаление слушателей событий
  • очистка ссылок на map
  • удаление таймеров
onRemove: function (map) {
    L.DomEvent.off(this._container);
}

Контролы, создающие внешние подписки (например, WebSocket или интервал), обязаны очищать их в onRemove.

Динамическое добавление и удаление контролов

Контролы могут добавляться и удаляться в рантайме:

const ctrl = L.control.myControl().addTo(map);

map.removeControl(ctrl);

Это позволяет строить интерфейсы, зависящие от состояния приложения.

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

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

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

  • общая шина событий
  • состояние карты (map как источник истины)
  • внешние менеджеры состояния

Ограничения системы контролов

Архитектура имеет ряд ограничений:

  • отсутствие встроенного state management
  • DOM-ориентированность без виртуального дерева
  • ручное управление событиями
  • необходимость самостоятельной оптимизации производительности при большом числе контролов

Несмотря на это, модель остаётся гибкой за счёт минималистичного API и прямого доступа к DOM и карте.