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

Встроенная система контролов в Mapbox GL JS опирается на единый контракт: любой элемент управления реализует интерфейс IControl. Этот подход позволяет расширять карту произвольными UI-компонентами — кнопками, панелями, переключателями слоёв, поисковыми строками и сложными виджетами, интегрированными в DOM карты.

Контролы добавляются через метод map.addControl(control, position), где position определяет расположение блока интерфейса:

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

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


Базовый контракт IControl

Минимальная реализация кастомного контрола включает два метода:

  • onAdd(map) — вызывается при добавлении на карту
  • onRemove() — вызывается при удалении

onAdd обязан вернуть DOM-элемент, который будет вставлен в интерфейс карты.

Минимальный пример

class SimpleButtonControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';

    const button = document.createElement('button');
    button.textContent = 'Клик';

    button.oncl ick = () => {
      this.map.zoomIn();
    };

    this.container.appendChild(button);

    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = undefined;
  }
}

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

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

Структура DOM-контрола

Mapbox GL JS ожидает, что корневой элемент контрола будет совместим со стандартными CSS-классами:

  • mapboxgl-ctrl — базовый класс
  • mapboxgl-ctrl-group — группа кнопок
  • mapboxgl-ctrl-icon — иконка-кнопка

Эти классы обеспечивают единообразный внешний вид и корректное позиционирование.

Пример кастомного контейнера

this.container = document.createElement('div');
this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group custom-control';

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

Контрол получает доступ к экземпляру карты через onAdd. Это позволяет:

  • изменять центр карты
  • управлять масштабом
  • слушать события
  • добавлять источники и слои

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

class FlyToControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';

    const button = document.createElement('button');
    button.textContent = 'Офис';

    button.oncl ick = () => {
      this.map.flyTo({
        center: [37.6173, 55.7558],
        zoom: 10
      });
    };

    this.container.appendChild(button);
    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = null;
  }
}

Подписка на события карты

Кастомные контролы часто зависят от событий карты. Подписка осуществляется внутри onAdd.

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

class MousePositionControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl';
    this.container.textContent = '0, 0';

    this.mouseMoveHandler = (e) => {
      const lng = e.lngLat.lng.toFixed(4);
      const lat = e.lngLat.lat.toFixed(4);
      this.container.textContent = `${lng}, ${lat}`;
    };

    map.on('mousemove', this.mouseMoveHandler);

    return this.container;
  }

  onRemove() {
    this.map.off('mousemove', this.mouseMoveHandler);
    this.container.remove();
    this.map = null;
  }
}

Ключевой момент: обязательное освобождение обработчиков в onRemove, иначе возникает утечка памяти.


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

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

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

class StyleSwitcherControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';

    const streets = document.createElement('button');
    streets.textContent = 'Streets';

    const satellite = document.createElement('button');
    satellite.textContent = 'Satellite';

    streets.oncl ick = () => {
      this.map.setStyle('mapbox://styles/mapbox/streets-v11');
    };

    satellite.oncl ick = () => {
      this.map.setStyle('mapbox://styles/mapbox/satellite-v9');
    };

    this.container.appendChild(streets);
    this.container.appendChild(satellite);

    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = null;
  }
}

При смене стиля важно учитывать, что все кастомные слои и источники будут пересозданы.


Асинхронные контролы

Контролы могут выполнять асинхронные операции: загрузку данных, API-запросы, геокодирование.

Пример: поиск по API

class SearchControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl';

    const input = document.createElement('input');
    input.placeholder = 'Поиск...';

    const results = document.createElement('div');

    input.onin put = async () => {
      const query = input.value;
      if (query.length < 3) return;

      const res = await fetch(`/api/search?q=${query}`);
      const data = await res.json();

      results.innerHTML = '';

      data.features.forEach(f => {
        const item = document.createElement('div');
        item.textContent = f.name;

        item.oncl ick = () => {
          this.map.flyTo({
            center: f.center,
            zoom: 12
          });
        };

        results.appendChild(item);
      });
    };

    this.container.appendChild(input);
    this.container.appendChild(results);

    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = null;
  }
}

Позиционирование и визуальная интеграция

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

  • верхние контролы перекрывают атрибуцию при нехватке места
  • нижние контролы должны учитывать масштабируемость интерфейса
  • несколько контролов в одной позиции группируются автоматически
map.addControl(new MyControl(), 'bottom-left');

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

Контролы часто управляют слоями карты:

  • включение/выключение visibility
  • фильтрация данных
  • динамическое добавление источников

Пример: чекбокс слоёв

class LayerToggleControl {
  onAdd(map) {
    this.map = map;

    this.container = document.createElement('div');
    this.container.className = 'mapboxgl-ctrl';

    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.checked = true;

    const label = document.createElement('label');
    label.textContent = 'Показывать POI';

    checkbox.oncha nge = () => {
      this.map.setLayoutProperty(
        'poi-layer',
        'visibility',
        checkbox.checked ? 'visible' : 'none'
      );
    };

    this.container.appendChild(checkbox);
    this.container.appendChild(label);

    return this.container;
  }

  onRemove() {
    this.container.remove();
    this.map = null;
  }
}

Рекомендации по архитектуре контролов

Кастомные контролы становятся частью UI-архитектуры карты, поэтому важны структурные принципы:

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

Работа с CSS и кастомным оформлением

Mapbox GL JS не ограничивает стилизацию контролов, но базовые классы должны сохраняться.

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

.custom-control button {
  background: #1e1e1e;
  color: white;
  border: none;
  padding: 6px 10px;
  cursor: pointer;
}

.custom-control button:hover {
  background: #333;
}

Контейнер:

this.container.className = 'mapboxgl-ctrl custom-control';

Ошибки и типичные проблемы

  • утечка памяти из-за неотписанных событий
  • повторная инициализация контролов при смене стиля карты
  • конфликт DOM-структуры с mapboxgl-ctrl-group
  • блокировка UI из-за синхронных тяжёлых операций в обработчиках
  • отсутствие защиты от пустого состояния карты при onRemove

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

Контролы могут быть объединены логически через общий контейнер или независимую регистрацию:

map.addControl(new ZoomControl(), 'top-right');
map.addControl(new FilterControl(), 'top-right');
map.addControl(new LegendControl(), 'bottom-right');

Mapbox автоматически группирует элементы, сохраняя порядок добавления.