Симуляция событий

Событийная система построена на интерфейсе Evented, который реализует подписку, удаление подписки и эмиссию событий. Любой экземпляр карты является источником событий: map.on, map.off, map.once, а также низкоуровневым механизмом map.fire, через который можно программно инициировать события.

События в Mapbox GL JS делятся на несколько категорий:

  • события загрузки (load, idle, render)
  • пользовательские взаимодействия (click, mousemove, drag)
  • события источников данных (data, sourcedata)
  • события слоёв (styledata, sourcedata)
  • служебные события (error, remove)

Симуляция событий опирается на возможность вручную генерировать эти сигналы без участия реального ввода пользователя или рендера браузера.


Механизм fire и внутренний цикл событий

Внутри Mapbox GL JS каждый объект Map наследует метод:

map.fire(type, eventData);

Этот метод напрямую вызывает обработчики, зарегистрированные через map.on(type, handler).

Базовая структура вызова:

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

map.fire('click', {
    lngLat: { lng: 30.5, lat: 50.4 },
    point: { x: 120, y: 80 },
    originalEvent: null
});

С точки зрения системы, fire не отличает синтетическое событие от реального, если передана корректная структура объекта.


Структура события и обязательные поля

Для корректной симуляции важно соблюдать формат события, который ожидает обработчик.

Типичное событие click содержит:

  • type — тип события
  • target — экземпляр карты
  • lngLat — географические координаты
  • point — координаты пикселя
  • originalEvent — DOM-объект (может быть null при симуляции)
  • features — (опционально) найденные объекты слоя

Пример полного синтетического события:

map.fire('click', {
    type: 'click',
    target: map,
    lngLat: { lng: 37.6173, lat: 55.7558 },
    point: { x: 400, y: 300 },
    originalEvent: null,
    features: []
});

Симуляция пользовательского клика

Событие click чаще всего используется для взаимодействия с объектами слоя.

Симуляция включает два уровня:

  1. Генерация события
  2. Подмена результатов пространственного запроса
const lngLat = { lng: 12.4924, lat: 41.8902 };
const point = map.project(lngLat);

const features = map.queryRenderedFeatures(point, {
    layers: ['pois-layer']
});

map.fire('click', {
    lngLat,
    point,
    features,
    originalEvent: null
});

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


Подмена результатов queryRenderedFeatures

Внутри Mapbox GL JS обработчики кликов часто используют:

map.queryRenderedFeatures(point, options);

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

Типовой паттерн:

  • вычисление координат
  • получение feature-объектов
  • передача их в fire
function simulateFeatureClick(map, lngLat, layerId) {
    const point = map.project(lngLat);

    const features = map.queryRenderedFeatures(point, {
        layers: [layerId]
    });

    map.fire('click', {
        lngLat,
        point,
        features,
        originalEvent: null
    });
}

Симуляция событий наведения (hover)

Событие mousemove используется для интерактивного подсвета объектов.

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

Симуляция:

const lngLat = { lng: 2.3522, lat: 48.8566 };
const point = map.project(lngLat);

const features = map.queryRenderedFeatures(point, {
    layers: ['cities-layer']
});

map.fire('mousemove', {
    lngLat,
    point,
    features,
    originalEvent: null
});

Дополнительно может симулироваться событие mouseleave, если требуется имитация ухода курсора:

map.fire('mouseleave', {
    lngLat,
    point,
    features: [],
    originalEvent: null
});

Программная симуляция drag-сценариев

События перетаскивания состоят из последовательности:

  • mousedown
  • mousemove
  • mouseup

Симуляция строится как цепочка вызовов fire.

map.fire('mousedown', {
    lngLat: start,
    point: map.project(start),
    originalEvent: null
});

map.fire('mousemove', {
    lngLat: mid,
    point: map.project(mid),
    originalEvent: null
});

map.fire('mouseup', {
    lngLat: end,
    point: map.project(end),
    originalEvent: null
});

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


Синтетические события для тестирования слоёв

Слои в Mapbox GL JS часто зависят от состояния карты и взаимодействий.

Для проверки логики можно вручную генерировать события, связанные с конкретными слоями:

map.fire('click', {
    lngLat: { lng: -74.006, lat: 40.7128 },
    point: { x: 200, y: 150 },
    features: [
        {
            type: 'Feature',
            properties: { id: 1, name: 'Test' },
            geometry: {
                type: 'Point',
                coordinates: [-74.006, 40.7128]
            },
            layer: { id: 'cities-layer' }
        }
    ],
    originalEvent: null
});

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


Интеграция с setFeatureState при симуляции

Часто события используются вместе с динамическим состоянием объектов:

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

При симуляции событий логика может включать:

  • установка состояния перед fire
  • сброс состояния после mouseleave
map.setFeatureState({ source: 'cities', id: 1 }, { hover: true });

map.fire('mousemove', {
    lngLat,
    point,
    features,
    originalEvent: null
});

map.setFeatureState({ source: 'cities', id: 1 }, { hover: false });

Использование fire для событий загрузки и состояния карты

Некоторые системные события также могут быть симулированы:

map.fire('load');
map.fire('idle');
map.fire('render');

Однако такие вызовы влияют только на обработчики, но не запускают внутренние процессы рендеринга, поэтому используются преимущественно в тестовых окружениях.


Ограничения симуляции событий

Система событий Mapbox GL JS имеет внутренние зависимости от WebGL и реального состояния рендера:

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

Событие существует только на уровне логики подписчиков.


Синхронизация координат при симуляции

При работе с синтетическими событиями критически важно согласование:

  • lngLat (географические координаты)
  • point (экранные координаты)
const lngLat = { lng: 139.6917, lat: 35.6895 };
const point = map.project(lngLat);

Ошибка в этой паре приводит к рассинхронизации логики:

  • неверные features
  • некорректные подсветки
  • ложные hit-test результаты

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

Симуляция редко ограничивается одиночным событием. Типовой сценарий включает:

  • подготовку координат
  • вычисление features
  • последовательность событий
  • обновление состояния слоя
function simulateInteraction(map, lngLat, layerId) {
    const point = map.project(lngLat);

    const features = map.queryRenderedFeatures(point, {
        layers: [layerId]
    });

    map.fire('mousemove', { lngLat, point, features, originalEvent: null });

    map.setFeatureState(
        { source: layerId, id: features[0]?.id },
        { active: true }
    );

    map.fire('click', { lngLat, point, features, originalEvent: null });
}

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