Touch события

Мобильное взаимодействие в MapLibre GL JS основано на системе сенсорных событий, которые интерпретируются картой как высокоуровневые жесты: перемещение, масштабирование, вращение и наклон. В отличие от классических DOM touch-событий, библиотека не предоставляет прямой модели «сырых касаний» как основной способ работы. Вместо этого используется абстракция, связывающая сенсорный ввод с состоянием камеры карты.

Архитектура обработки касаний

Внутри MapLibre GL JS сенсорные события проходят несколько уровней обработки:

  • перехват DOM-событий (touchstart, touchmove, touchend, touchcancel)
  • преобразование координат касаний в экранные точки canvas
  • интерпретация жестов (pan, pinch, rotate)
  • обновление состояния камеры карты
  • генерация высокоуровневых событий карты (move, zoom, rotate, touchstart и др.)

Ключевая особенность заключается в том, что MapLibre ориентируется не на отдельные касания, а на их комбинации. Например, два пальца интерпретируются как жест масштабирования и вращения, а один палец — как перемещение карты.

Базовые touch-события карты

MapLibre GL JS предоставляет набор событий, связанных с сенсорным вводом:

  • touchstart — начало касания
  • touchmove — движение пальцев по экрану
  • touchend — завершение касания
  • touchcancel — прерывание касания системой

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

map.on('touchstart', (e) => {
    console.log('Начало касания', e);
});

map.on('touchmove', (e) => {
    console.log('Движение касания', e);
});

map.on('touchend', (e) => {
    console.log('Конец касания', e);
});

Каждое событие содержит объект, включающий информацию о состоянии карты и исходных DOM-событиях.

Структура объекта события

Событие MapLibre, связанное с касаниями, содержит следующие важные поля:

  • type — тип события
  • target — экземпляр карты
  • point — координаты в пикселях относительно canvas
  • lngLat — географические координаты точки касания
  • originalEvent — исходное DOM touch-событие
  • touches — массив текущих активных касаний

Пример анализа касания:

map.on('touchstart', (e) => {
    const firstTouch = e.originalEvent.touches[0];

    console.log('Pixel:', e.point);
    console.log('Coordinates:', e.lngLat);
    console.log('Screen touch:', firstTouch.clientX, firstTouch.clientY);
});

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

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

MapLibre GL JS интерпретирует сенсорный ввод как набор жестов, управляемых внутренними обработчиками:

  • dragPan — перемещение карты одним пальцем
  • pinchZoom — масштабирование двумя пальцами
  • rotate — вращение жестом двух пальцев
  • doubleTapZoom — приближение двойным тапом
  • touchZoomRotate — комбинированное управление масштабом и вращением

Эти режимы контролируются через map.touchZoomRotate и общие настройки взаимодействия:

map.touchZoomRotate.disable(); // отключение pinch/rotate
map.touchZoomRotate.enable();  // включение обратно

Также возможно полное управление интерактивностью:

map.dragPan.disable();
map.scrollZoom.disable();
map.boxZoom.disable();
map.keyboard.disable();

Различие между touch и pointer подходом

Хотя браузеры поддерживают Pointer Events, MapLibre GL JS исторически использует абстракцию поверх touch-событий. Это означает:

  • touch-события агрегируются в жесты
  • pointer-уровень не является основным API
  • логика взаимодействия централизована в контроллерах карты

При этом разработчик может комбинировать DOM pointer events с API карты:

map.getCanvas().addEventListener('pointerdown', (e) => {
    console.log('Pointer event', e.pointerType);
});

Однако такие обработчики не влияют на внутреннюю механику жестов, если явно не вмешиваться в preventDefault.

Управление поведением браузера

Сенсорные события в браузерах часто сопровождаются стандартными действиями: прокрутка страницы, масштабирование viewport, жесты OS. MapLibre предотвращает эти эффекты через preventDefault внутри своих обработчиков.

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

map.getCanvas().addEventListener('touchmove', (e) => {
    e.preventDefault();
});

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

Особенно критично это для:

  • iOS Safari (жесты прокрутки)
  • Android Chrome (overscroll behavior)
  • гибридных WebView

Множественные касания и их интерпретация

MapLibre отслеживает массив активных касаний и строит из них жесты:

  • 1 палец → pan
  • 2 пальца → pinch zoom + rotate
  • 3+ пальцев → блокировка или игнорирование (в зависимости от конфигурации)

Пример анализа количества касаний:

map.on('touchmove', (e) => {
    const touches = e.originalEvent.touches.length;

    if (touches === 1) {
        console.log('Перемещение карты');
    }

    if (touches === 2) {
        console.log('Масштабирование/вращение');
    }
});

Координатная система касаний

Каждое касание в MapLibre проходит преобразование:

  1. screen coordinates (clientX / clientY)
  2. canvas pixel coordinates
  3. world coordinates (Web Mercator)
  4. geographic coordinates (lngLat)

Это позволяет напрямую использовать данные жестов для геопривязанных операций:

map.on('touchend', (e) => {
    const coords = e.lngLat;

    map.addSource('touch-point', {
        type: 'geojson',
        data: {
            type: 'Point',
            coordinates: [coords.lng, coords.lat]
        }
    });
});

Конфликты жестов и приоритеты

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

  • вращение требует двух пальцев и активного режима rotate
  • zoom имеет приоритет над pan при pinch
  • pan активен только при одном касании

Конфигурация позволяет управлять этими приоритетами:

const map = new maplibregl.Map({
    container: 'map',
    interactive: true,
    dragRotate: false,
    touchPitch: false
});

Отключение dragRotate снижает вероятность конфликтов на мобильных устройствах.

Производительность сенсорных событий

Сенсорные события являются высокочастотными, особенно touchmove. В MapLibre они обрабатываются в тесной связке с render loop WebGL.

Критически важно:

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

Оптимизированный подход:

let lastLngLat = null;

map.on('touchmove', (e) => {
    lastLngLat = e.lngLat;
});

map.on('touchend', () => {
    console.log('Final position', lastLngLat);
});

Кастомные жесты поверх touch API

Несмотря на встроенную систему жестов, можно реализовывать собственные взаимодействия, опираясь на touchstart и touchmove.

Пример определения свайпа:

let startX, startY;

map.on('touchstart', (e) => {
    const t = e.originalEvent.touches[0];
    startX = t.clientX;
    startY = t.clientY;
});

map.on('touchend', (e) => {
    const t = e.originalEvent.changedTouches[0];

    const dx = t.clientX - startX;
    const dy = t.clientY - startY;

    if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
        console.log('Swipe detected');
    }
});

Такие решения требуют осторожности, поскольку могут конфликтовать с встроенными жестами карты.

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

Поведение touch-событий сильно зависит от платформы:

  • iOS Safari может задерживать click после touchend
  • Android Chrome часто генерирует дополнительные synthetic events
  • некоторые WebView добавляют собственные слои интерпретации жестов

MapLibre учитывает эти особенности через унифицированный слой обработки событий.

Связь touch-событий с состоянием камеры

Каждое сенсорное взаимодействие напрямую изменяет камеру карты:

  • center — смещение карты при pan
  • zoom — масштаб при pinch
  • bearing — угол поворота
  • pitch — наклон

Пример отслеживания изменений:

map.on('move', () => {
    const center = map.getCenter();
    const zoom = map.getZoom();
    const bearing = map.getBearing();

    console.log(center, zoom, bearing);
});

Touch-события выступают триггером, а move — результатом трансформации камеры.

Взаимодействие с canvas слоем

Все касания происходят поверх WebGL canvas, который MapLibre создаёт внутри контейнера карты. Для доступа к низкоуровневым событиям используется:

const canvas = map.getCanvas();

canvas.addEventListener('touchstart', (e) => {
    console.log('Canvas touch');
});

Однако прямое взаимодействие с canvas требует осторожности, так как может нарушить внутреннюю логику жестов.

Расширение модели взаимодействия

Touch-система MapLibre проектировалась как расширяемая. Через комбинацию:

  • map.on('touch*')
  • map.dragPan
  • map.touchZoomRotate
  • DOM event listeners

можно построить альтернативные модели взаимодействия, включая:

  • режим «только просмотр»
  • режим «рисования по карте»
  • режим «ограниченного pan без zoom»
  • кастомные gesture-driven интерфейсы

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