Picture-in-picture подход в картографии представляет собой
одновременное отображение двух картографических контекстов в одном
интерфейсе: основной карты и уменьшенного вспомогательного окна. В
MapLibre GL JS это реализуется через несколько экземпляров
Map, синхронизированных между собой по состоянию камеры
(center, zoom, bearing, pitch). Такая архитектура применяется для
создания обзорных мини-карт, навигационных подсказок, аналитических
вставок и контекстных географических индикаторов.
Базовая модель построения picture-in-picture основана на двух независимых WebGL-контекстах:
mainMap)pipMap)Каждый экземпляр maplibregl.Map управляет собственным
canvas-элементом и рендерит сцену независимо, но состояние камеры может
быть синхронизировано вручную через API.
Ключевая особенность подхода — отсутствие встроенного механизма PiP в MapLibre GL JS. Реализация полностью строится на уровне приложения.
Типовая структура включает контейнер основной карты и абсолютное позиционирование мини-карты поверх интерфейса:
<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-карта обычно отображает упрощённый стиль:
Пример кастомного стиля:
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, поэтому применяются оптимизации:
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
});
});
Мини-карта не нуждается в высокой детализации:
jumpTo вместо easeTojumpTo исключает анимации и снижает количество
промежуточных кадров.
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;
});
Поведение мини-карты может быть расширено через 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 и не влияет на рендеринг карты.
В некоторых сценариях используется синхронизация через bounding box:
const bounds = mainMap.getBounds();
pipMap.fitBounds(bounds, {
padding: 20,
linear: true
});
Это позволяет сохранять одинаковую видимую область независимо от соотношения сторон контейнеров.
При активной мини-карте важно блокировать лишние события:
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 как WebGL canvas поверх основной карты без второго экземпляра MapLibre:
mapbox-gl style APIТакой метод снижает нагрузку, но ограничивает независимость камер.
Поведение PiP часто зависит от zoom:
function updatePipZoom() {
const z = mainMap.getZoom();
pipMap.setZoom(Math.max(1, z - 4));
}
При использовании нестандартных проекций важно синхронизировать:
Несогласованность параметров приводит к смещению мини-карты относительно основной.