Picture-in-picture карты

Picture-in-picture подход в картографии представляет собой одновременное отображение двух картографических контекстов в одном интерфейсе: основной карты и уменьшенного вспомогательного окна. В MapLibre GL JS это реализуется через несколько экземпляров Map, синхронизированных между собой по состоянию камеры (center, zoom, bearing, pitch). Такая архитектура применяется для создания обзорных мини-карт, навигационных подсказок, аналитических вставок и контекстных географических индикаторов.


Архитектура PiP в MapLibre GL JS

Базовая модель построения picture-in-picture основана на двух независимых WebGL-контекстах:

  • основная карта (mainMap)
  • вспомогательная карта (pipMap)

Каждый экземпляр maplibregl.Map управляет собственным canvas-элементом и рендерит сцену независимо, но состояние камеры может быть синхронизировано вручную через API.

Ключевая особенность подхода — отсутствие встроенного механизма PiP в MapLibre GL JS. Реализация полностью строится на уровне приложения.


DOM-структура и позиционирование слоя PiP

Типовая структура включает контейнер основной карты и абсолютное позиционирование мини-карты поверх интерфейса:

<div id="map"></div>
<div id="pip"></div>

CSS-слой определяет поведение PiP-окна:

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

#pip {
  position: absolute;
  width: 240px;
  height: 160px;
  right: 16px;
  bottom: 16px;
  border: 1px solid rgba(0,0,0,0.2);
  overflow: hidden;
  box-shadow: 0 6px 24px rgba(0,0,0,0.25);
}

Мини-карта всегда располагается поверх основной, не влияя на её layout и не участвуя в потоковой разметке страницы.


Инициализация двух карт

Создание PiP-структуры начинается с двух независимых экземпляров карты:

const mainMap = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [37.6173, 55.7558],
  zoom: 9
});

const pipMap = new maplibregl.Map({
  container: 'pip',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [37.6173, 55.7558],
  zoom: 3,
  interactive: false
});

Мини-карта обычно запускается в режиме interactive: false, если требуется только визуальный обзор без взаимодействия.


Синхронизация камеры

Основной механизм PiP реализуется через обработку событий движения камеры.

Синхронизация основной → мини-карта

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

  pipMap.jumpTo({
    center,
    zoom: zoom - 3,
    bearing: 0,
    pitch: 0
  });
});

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


Обратная синхронизация мини-карты

При включении интерактивности PiP-окна возможна обратная связь:

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

  mainMap.easeTo({
    center,
    duration: 200
  });
});

Чтобы избежать бесконечных циклов обновления, применяется флаг блокировки:

let syncing = false;

mainMap.on('move', () => {
  if (syncing) return;
  syncing = true;

  pipMap.jumpTo({
    center: mainMap.getCenter(),
    zoom: mainMap.getZoom() - 3
  });

  syncing = false;
});

Разделение уровней детализации

PiP-карта обычно отображает упрощённый стиль:

  • уменьшенная насыщенность слоёв
  • отключение анимаций
  • скрытие сложных символов
  • отсутствие 3D-слоёв

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

const pipMap = new maplibregl.Map({
  container: 'pip',
  style: {
    version: 8,
    sources: mainStyle.sources,
    layers: mainStyle.layers.filter(layer => layer.type !== 'fill-extrusion')
  },
  interactive: false
});

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


Поведение камеры и согласование параметров

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

  • основной карте соответствует точная географическая позиция
  • мини-карта отображает обобщённый обзор

Типичная формула масштабирования:

const pipZoom = mainMap.getZoom() - 4;

Дополнительно может фиксироваться:

  • bearing = 0 для устранения вращения
  • pitch = 0 для плоского обзора

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

Использование двух WebGL-контекстов увеличивает нагрузку на GPU, поэтому применяются оптимизации:

1. Ограничение частоты обновлений

let lastUpdate = 0;

mainMap.on('move', () => {
  const now = performance.now();
  if (now - lastUpdate < 50) return;
  lastUpdate = now;

  pipMap.jumpTo({
    center: mainMap.getCenter(),
    zoom: mainMap.getZoom() - 3
  });
});

2. Отключение лишних слоёв

Мини-карта не нуждается в высокой детализации:

  • отключение label layers
  • отключение heatmap
  • отключение 3D extrusion

3. Использование jumpTo вместо easeTo

jumpTo исключает анимации и снижает количество промежуточных кадров.


Интерактивные сценарии PiP

Picture-in-picture может выполнять разные роли:

Обзорная мини-карта

Отображает глобальный контекст текущей области.

Навигационная вставка

Показывает маршрут или альтернативный уровень масштаба.

Аналитическая вставка

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


Двусторонняя связь с ограничением зацикливания

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

let fromMain = false;
let fromPip = false;

mainMap.on('move', () => {
  if (fromPip) return;
  fromMain = true;

  pipMap.jumpTo({
    center: mainMap.getCenter(),
    zoom: mainMap.getZoom() - 3
  });

  fromMain = false;
});

pipMap.on('move', () => {
  if (fromMain) return;
  fromPip = true;

  mainMap.jumpTo({
    center: pipMap.getCenter(),
    zoom: pipMap.getZoom() + 3
  });

  fromPip = false;
});

Перетаскиваемое PiP-окно

Поведение мини-карты может быть расширено через drag-события DOM:

const pipContainer = document.getElementById('pip');

let dragging = false;
let offset = { x: 0, y: 0 };

pipContainer.addEventListener('mousedown', (e) => {
  dragging = true;
  offset.x = e.clientX - pipContainer.offsetLeft;
  offset.y = e.clientY - pipContainer.offsetTop;
});

window.addEventListener('mousemove', (e) => {
  if (!dragging) return;

  pipContainer.style.left = `${e.clientX - offset.x}px`;
  pipContainer.style.top = `${e.clientY - offset.y}px`;
});

window.addEventListener('mouseup', () => {
  dragging = false;
});

Такой слой управления отделён от логики MapLibre GL JS и не влияет на рендеринг карты.


Синхронизация bounds вместо center

В некоторых сценариях используется синхронизация через bounding box:

const bounds = mainMap.getBounds();

pipMap.fitBounds(bounds, {
  padding: 20,
  linear: true
});

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


Изоляция событий взаимодействия

При активной мини-карте важно блокировать лишние события:

  • scroll zoom может быть отключён
  • drag rotation отключается
  • touch gestures ограничиваются
const pipMap = new maplibregl.Map({
  container: 'pip',
  interactive: true,
  dragRotate: false,
  scrollZoom: false,
  doubleClickZoom: false
});

Переиспользование источников данных

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

mainMap.on('load', () => {
  const source = mainMap.getSource('points');

  pipMap.addSource('points', source.serialize());
});

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


Сценарии с различными стилями отображения

PiP-карта часто использует альтернативный стиль:

  • светлая карта внутри тёмного интерфейса
  • контурная карта без заливок
  • ночной режим с упрощённой геометрией

Разделение стилей позволяет повысить читаемость и снизить визуальный шум.


Расширение PiP через слой overlay

Дополнительный подход — наложение PiP как WebGL canvas поверх основной карты без второго экземпляра MapLibre:

  • один MapLibre instance
  • отдельный canvas overlay
  • ручной рендер мини-кадра через mapbox-gl style API

Такой метод снижает нагрузку, но ограничивает независимость камер.


Управление уровнем детализации при масштабировании

Поведение PiP часто зависит от zoom:

  • при высоком zoom основной карты PiP показывает регион
  • при низком zoom PiP показывает континент
function updatePipZoom() {
  const z = mainMap.getZoom();

  pipMap.setZoom(Math.max(1, z - 4));
}

Географическая согласованность и проекции

При использовании нестандартных проекций важно синхронизировать:

  • projection parameters
  • min/max zoom
  • world wrapping behavior

Несогласованность параметров приводит к смещению мини-карты относительно основной.