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

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

Основным методом регистрации обработчиков является on():

map.on('click', () => {
    console.log('Карта была нажата');
});

Для удаления обработчика используется метод off():

function handleClick() {
    console.log('Клик');
}

map.on('click', handleClick);
map.off('click', handleClick);

Для однократного выполнения применяется метод once():

map.once('load', () => {
    console.log('Карта загружена');
});

Такой обработчик автоматически удаляется после первого вызова.


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

Одной из наиболее важных групп событий являются события жизненного цикла карты.

Событие load

Срабатывает после полной инициализации карты и загрузки базового стиля.

map.on('load', () => {
    console.log('Карта готова к работе');
});

Большинство операций по добавлению слоев и источников рекомендуется выполнять именно внутри обработчика load.

map.on('load', () => {
    map.addSource('cities', {
        type: 'geojson',
        data: '/data/cities.geojson'
    });

    map.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities'
    });
});

Событие idle

Срабатывает, когда карта полностью завершила все операции рендеринга и загрузки данных.

map.on('idle', () => {
    console.log('Все данные загружены');
});

Событие полезно для выполнения тяжелых вычислений после окончания работы движка визуализации.

Событие remove

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

map.on('remove', () => {
    console.log('Карта уничтожена');
});

События мыши

Mapbox GL JS предоставляет полный набор событий для работы с мышью.

click

Срабатывает при нажатии кнопки мыши.

map.on('click', (event) => {
    console.log(event.lngLat);
});

Объект события содержит координаты точки клика.

map.on('click', (event) => {
    console.log(event.lngLat.lng);
    console.log(event.lngLat.lat);
});

dblclick

Срабатывает при двойном клике.

map.on('dblclick', (event) => {
    console.log('Двойной клик');
});

mousedown и mouseup

Используются для отслеживания нажатия и отпускания кнопки мыши.

map.on('mousedown', () => {
    console.log('Нажатие');
});

map.on('mouseup', () => {
    console.log('Отпускание');
});

mousemove

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

map.on('mousemove', (event) => {
    console.log(event.lngLat);
});

Частое срабатывание требует осторожности при выполнении ресурсоемких операций.

mouseenter и mouseleave

Используются для отслеживания входа и выхода курсора из слоя.

map.on('mouseenter', 'cities-layer', () => {
    map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities-layer', () => {
    map.getCanvas().style.cursor = '';
});

События сенсорных устройств

Для мобильных устройств доступны специальные события.

touchstart

map.on('touchstart', (event) => {
    console.log('Касание началось');
});

touchmove

map.on('touchmove', (event) => {
    console.log('Палец перемещается');
});

touchend

map.on('touchend', (event) => {
    console.log('Касание завершено');
});

Эти события позволяют реализовывать собственную логику работы с жестами.


События перемещения карты

Навигация по карте сопровождается большим количеством событий.

movestart

Срабатывает в начале перемещения.

map.on('movestart', () => {
    console.log('Начало перемещения');
});

move

Вызывается во время движения карты.

map.on('move', () => {
    console.log(map.getCenter());
});

moveend

Срабатывает после завершения перемещения.

map.on('moveend', () => {
    console.log('Перемещение завершено');
});

Практический пример:

map.on('moveend', () => {
    const center = map.getCenter();

    loadObjects(
        center.lng,
        center.lat
    );
});

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


События масштабирования

zoomstart

map.on('zoomstart', () => {
    console.log('Начало масштабирования');
});

zoom

map.on('zoom', () => {
    console.log(map.getZoom());
});

zoomend

map.on('zoomend', () => {
    console.log('Масштабирование завершено');
});

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

map.on('zoom', () => {
    document.getElementById('zoom').textContent =
        map.getZoom().toFixed(2);
});

События вращения карты

rotatestart

map.on('rotatestart', () => {
    console.log('Начало вращения');
});

rotate

map.on('rotate', () => {
    console.log(map.getBearing());
});

rotateend

map.on('rotateend', () => {
    console.log('Вращение завершено');
});

Получение текущего угла поворота:

map.on('rotate', () => {
    const bearing = map.getBearing();

    console.log(bearing);
});

События изменения наклона

pitchstart

map.on('pitchstart', () => {
    console.log('Начало изменения наклона');
});

pitch

map.on('pitch', () => {
    console.log(map.getPitch());
});

pitchend

map.on('pitchend', () => {
    console.log('Наклон изменен');
});

События изменения размера

Если размеры контейнера изменяются, карта генерирует соответствующее событие.

resize

map.on('resize', () => {
    console.log('Размер карты изменился');
});

Пример адаптации интерфейса:

map.on('resize', () => {
    updateSidebarLayout();
});

Работа с объектом события

Большинство обработчиков получают объект события.

Пример структуры:

map.on('click', (event) => {
    console.log(event.point);
    console.log(event.lngLat);
    console.log(event.originalEvent);
});

point

Позиция курсора в пикселях.

map.on('click', (event) => {
    console.log(event.point.x);
    console.log(event.point.y);
});

lngLat

Географические координаты.

map.on('click', (event) => {
    console.log(event.lngLat.lng);
    console.log(event.lngLat.lat);
});

originalEvent

Оригинальное DOM-событие браузера.

map.on('click', (event) => {
    console.log(event.originalEvent);
});

События слоев

Mapbox GL JS позволяет подписываться на события конкретного слоя.

map.on('click', 'cities-layer', (event) => {
    console.log(event.features);
});

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

Получение выбранного объекта

map.on('click', 'cities-layer', (event) => {
    const feature = event.features[0];

    console.log(feature.properties.name);
});

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

События часто используются вместе с Popup.

map.on('click', 'cities-layer', (event) => {
    const feature = event.features[0];

    new mapboxgl.Popup()
        .setLngLat(event.lngLat)
        .setHTML(`
            <h3>${feature.properties.name}</h3>
        `)
        .addTo(map);
});

Подсветка объектов при наведении

Один из самых распространенных сценариев.

map.on('mouseenter', 'cities-layer', () => {
    map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities-layer', () => {
    map.getCanvas().style.cursor = '';
});

Подсветка выбранного объекта:

map.on('mousemove', 'cities-layer', (event) => {
    const featureId = event.features[0].id;

    map.setFeatureState(
        {
            source: 'cities',
            id: featureId
        },
        {
            hover: true
        }
    );
});

События загрузки данных

sourcedata

Срабатывает при изменении данных источника.

map.on('sourcedata', (event) => {
    console.log(event.sourceId);
});

data

Универсальное событие работы с данными.

map.on('data', (event) => {
    console.log(event.dataType);
});

styledata

Вызывается после изменения стиля.

map.on('styledata', () => {
    console.log('Стиль обновлен');
});

Отслеживание завершения загрузки GeoJSON

map.on('sourcedata', (event) => {
    if (
        event.sourceId === 'cities' &&
        event.isSourceLoaded
    ) {
        console.log('GeoJSON загружен');
    }
});

Такой подход полезен при последовательной загрузке нескольких наборов данных.


События ошибок

Для обработки ошибок предусмотрено событие error.

map.on('error', (event) => {
    console.error(event.error);
});

Пример централизованного логирования:

map.on('error', ({ error }) => {
    sendErrorToServer(error);
});

Комбинирование нескольких событий

Нередко требуется отслеживать несколько состояний одновременно.

let isDragging = false;

map.on('movestart', () => {
    isDragging = true;
});

map.on('moveend', () => {
    isDragging = false;
});

Другой пример:

map.on('zoomend', updateData);
map.on('moveend', updateData);

function updateData() {
    console.log('Обновление данных');
}

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

Регистрация обработчиков может происходить во время выполнения приложения.

function enableTracking() {
    map.on('mousemove', trackCursor);
}

function disableTracking() {
    map.off('mousemove', trackCursor);
}

function trackCursor(event) {
    console.log(event.lngLat);
}

Такой подход позволяет уменьшать нагрузку на приложение.


Паттерн делегирования событий

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

map.on('click', (event) => {
    const features = map.queryRenderedFeatures(
        event.point
    );

    if (!features.length) {
        return;
    }

    const feature = features[0];

    switch (feature.layer.id) {
        case 'cities-layer':
            showCity(feature);
            break;

        case 'roads-layer':
            showRoad(feature);
            break;
    }
});

Преимущества подхода:

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

Практические рекомендации

Использование moveend вместо move

Событие move может вызываться десятки раз в секунду.

Неэффективный вариант:

map.on('move', loadData);

Предпочтительный вариант:

map.on('moveend', loadData);

Очистка обработчиков

При удалении компонентов интерфейса необходимо снимать подписки.

map.off('click', handleClick);

Это предотвращает утечки памяти.

Минимизация тяжелых операций

Нежелательно выполнять сетевые запросы внутри событий:

map.on('mousemove', () => {
    fetch('/api/data');
});

Лучше использовать ограничение частоты вызовов:

const throttledUpdate = throttle(updateData, 300);

map.on('mousemove', throttledUpdate);

Использование именованных функций

Вместо анонимных обработчиков:

map.on('click', function(event) {
    processFeature(event);
});

Предпочтительно:

function handleMapClick(event) {
    processFeature(event);
}

map.on('click', handleMapClick);

Такой код легче тестировать, переиспользовать и удалять через off().