Отслеживание движения

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

Ключевые параметры камеры:

  • center — географический центр (долгота/широта)
  • zoom — уровень масштаба
  • bearing — угол поворота карты (в градусах)
  • pitch — наклон камеры

Эти параметры можно получить напрямую:

const center = map.getCenter();
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();

Любая анимация или пользовательское взаимодействие сводится к изменению одного или нескольких этих значений.


События, фиксирующие движение карты

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

Основные события:

  • movestart — начало любого движения
  • move — изменение положения камеры
  • moveend — завершение движения
  • zoomstart, zoom, zoomend — масштабирование
  • rotatestart, rotate, rotateend — вращение
  • pitchstart, pitch, pitchend — наклон

Непрерывное отслеживание движения

map.on('move', () => {
  const center = map.getCenter();
  console.log('Центр:', center.lng, center.lat);
});

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

Отслеживание завершённого состояния

map.on('moveend', () => {
  const state = {
    center: map.getCenter(),
    zoom: map.getZoom(),
    bearing: map.getBearing(),
    pitch: map.getPitch()
  };

  console.log('Финальное состояние камеры:', state);
});

moveend применяется для фиксации итогового положения камеры после анимации или взаимодействия пользователя.


Разделение пользовательского и программного движения

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

let isProgrammatic = false;

function moveCamera() {
  isProgrammatic = true;

  map.easeTo({
    center: [30, 50],
    zoom: 10
  });

  setTimeout(() => {
    isProgrammatic = false;
  }, 300);
}

map.on('move', () => {
  if (isProgrammatic) {
    console.log('Движение вызвано кодом');
  } else {
    console.log('Движение пользователя');
  }
});

Такой подход используется для аналитики и синхронизации интерфейсов.


Плавные переходы камеры

Для анимации движения используются методы:

  • flyTo — полёт к точке
  • easeTo — плавное перемещение
  • jumpTo — мгновенное перемещение

easeTo как базовый инструмент

map.easeTo({
  center: [60, 55],
  zoom: 8,
  duration: 1000
});

flyTo для сложных траекторий

map.flyTo({
  center: [60, 55],
  zoom: 9,
  speed: 1.2,
  curve: 1.5
});

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


Низкоуровневое отслеживание движения через requestAnimationFrame

Иногда требуется фиксировать положение камеры с максимальной точностью, например для синхронизации с 3D-объектами или анимациями интерфейса.

function trackCamera() {
  const center = map.getCenter();
  const zoom = map.getZoom();
  const bearing = map.getBearing();

  updateOverlay(center, zoom, bearing);

  requestAnimationFrame(trackCamera);
}

trackCamera();

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


Интерполяция движения и сглаживание значений

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

Для сглаживания используется интерполяция:

let lastCenter = null;

map.on('move', () => {
  const current = map.getCenter();

  if (lastCenter) {
    const smoothLng = lastCenter.lng * 0.8 + current.lng * 0.2;
    const smoothLat = lastCenter.lat * 0.8 + current.lat * 0.2;

    renderMarker([smoothLng, smoothLat]);
  }

  lastCenter = current;
});

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


Привязка интерфейса к движению карты

Частая задача — синхронизация DOM-элементов с движением карты: тултипы, панели, индикаторы.

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

  infoBox.textContent = `
    lng: ${center.lng.toFixed(4)}
    lat: ${center.lat.toFixed(4)}
  `;
});

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

map.on('move', () => {
  const zoom = map.getZoom();
  scaleIndicator.style.width = `${zoom * 10}px`;
});

Отслеживание траектории перемещения камеры

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

const path = [];

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

  path.push({
    lng: center.lng,
    lat: center.lat,
    time: Date.now()
  });
});

Далее массив может использоваться для построения линии движения или аналитики.


Управление режимом следования (follow mode)

Режим «следования» фиксирует камеру на определённой точке, например на объекте или координате GPS.

let follow = true;

function updatePosition(lng, lat) {
  if (follow) {
    map.easeTo({
      center: [lng, lat],
      duration: 500
    });
  }
}

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


Геолокация и отслеживание реального движения

MapLibre GL JS поддерживает встроенный контрол геолокации.

map.addControl(
  new maplibregl.GeolocateControl({
    positionOptions: {
      enableHighAccuracy: true
    },
    trackUserLocation: true
  })
);

При включённом trackUserLocation карта автоматически обновляет позицию пользователя, что фактически превращает её в систему слежения за движением в реальном мире.


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

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

let last = null;

map.on('move', () => {
  const current = map.getCenter();
  const time = Date.now();

  if (last) {
    const dx = current.lng - last.center.lng;
    const dy = current.lat - last.center.lat;
    const dt = time - last.time;

    const speed = Math.sqrt(dx * dx + dy * dy) / dt;

    console.log('Скорость движения камеры:', speed);
  }

  last = { center: current, time };
});

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


Синхронизация нескольких карт

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

function sync(source, target) {
  source.on('move', () => {
    const center = source.getCenter();
    const zoom = source.getZoom();
    const bearing = source.getBearing();

    target.jumpTo({ center, zoom, bearing });
  });
}

sync(mapA, mapB);

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


Блокировка движения и контроль состояния камеры

Иногда требуется полностью зафиксировать камеру:

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

После этого движение возможно только программно:

map.easeTo({
  center: [40, 40],
  zoom: 5
});

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


Отслеживание инерции и завершения анимации

После пользовательского взаимодействия карта может продолжать движение по инерции. Для фиксации окончания используется moveend.

map.on('moveend', () => {
  console.log('Камера стабилизировалась');
});

Это событие критично для запуска логики, зависящей от финального положения.


Координация движения камеры с 3D-слоями

При использовании наклона (pitch) и вращения (bearing) важно синхронизировать 3D-объекты с камерой.

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

  model.rotation.z = -bearing * Math.PI / 180;
});

Без такой синхронизации 3D-слои будут визуально «отставать» от карты.


Контроль частоты обработки движения

Для оптимизации нагрузки обработку move часто ограничивают:

let ticking = false;

map.on('move', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      const center = map.getCenter();
      updateUI(center);
      ticking = false;
    });

    ticking = true;
  }
});

Это предотвращает перегрузку при частых событиях.


Привязка анимаций к состоянию камеры

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

map.on('move', () => {
  const zoom = map.getZoom();

  const scale = Math.pow(2, zoom);
  element.style.transform = `scale(${scale})`;
});

Таким образом камера становится управляющим фактором всей сцены отображения.