Множественные карты

MapLibre GL JS поддерживает одновременное создание множества независимых экземпляров карты на одной странице. Каждый экземпляр работает в собственном DOM-контейнере и имеет отдельный WebGL-контекст, слой состояния, набор источников данных и обработчиков событий.

Базовая схема создания нескольких карт заключается в инициализации отдельных объектов map с разными контейнерами:

import maplibregl from "maplibre-gl";

const mapA = new maplibregl.Map({
    container: "mapA",
    style: "https://demotiles.maplibre.org/style.json",
    center: [37.6173, 55.7558],
    zoom: 10
});

const mapB = new maplibregl.Map({
    container: "mapB",
    style: "https://demotiles.maplibre.org/style.json",
    center: [2.3522, 48.8566],
    zoom: 10
});

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


Управление контейнерами и жизненным циклом

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

<div id="mapA" class="map"></div>
<div id="mapB" class="map"></div>
.map {
    width: 50%;
    height: 400px;
    display: inline-block;
}

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

mapA.remove();
mapB.remove();

Метод remove() освобождает WebGL-контекст, удаляет обработчики событий и предотвращает утечки памяти при повторной инициализации.


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

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

Основной механизм — обработка события move:

function syncMaps(source, target) {
    const center = source.getCenter();
    const zoom = source.getZoom();
    const bearing = source.getBearing();
    const pitch = source.getPitch();

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

mapA.on("move", () => syncMaps(mapA, mapB));
mapB.on("move", () => syncMaps(mapB, mapA));

Для предотвращения рекурсивных вызовов применяется флаг блокировки:

let syncing = false;

function sync(from, to) {
    if (syncing) return;
    syncing = true;

    to.jumpTo(from.getCenter(), from.getZoom());

    syncing = false;
}

mapA.on("move", () => sync(mapA, mapB));
mapB.on("move", () => sync(mapB, mapA));

Синхронизация масштаба и ограничение событий

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

  • moveend
  • zoomend
  • rotateend
mapA.on("moveend", () => {
    mapB.jumpTo({
        center: mapA.getCenter(),
        zoom: mapA.getZoom()
    });
});

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


Мини-карта (overview map)

Распространённый паттерн — основная карта и миниатюрный обзорный экземпляр. Мини-карта отображает глобальный контекст и синхронизируется с основной.

const mainMap = new maplibregl.Map({
    container: "main",
    style: styleUrl,
    center: [30, 50],
    zoom: 5
});

const overviewMap = new maplibregl.Map({
    container: "overview",
    style: styleUrl,
    center: [30, 50],
    zoom: 1
});

mainMap.on("move", () => {
    overviewMap.jumpTo({
        center: mainMap.getCenter(),
        zoom: 1
    });
});

Мини-карта обычно имеет отключённые интерактивные элементы:

new maplibregl.Map({
    container: "overview",
    style: styleUrl,
    interactive: false
});

Сравнительные карты и split-screen режим

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

const mapLeft = new maplibregl.Map({
    container: "left",
    style: "style-day.json"
});

const mapRight = new maplibregl.Map({
    container: "right",
    style: "style-night.json"
});

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

function linkMaps(a, b) {
    a.on("move", () => {
        b.jumpTo({
            center: a.getCenter(),
            zoom: a.getZoom(),
            bearing: a.getBearing()
        });
    });
}

linkMaps(mapLeft, mapRight);
linkMaps(mapRight, mapLeft);

Разделение состояния и независимость слоёв

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

mapA.on("load", () => {
    mapA.addSource("points", {
        type: "geojson",
        data: pointsGeoJSON
    });

    mapA.addLayer({
        id: "points-layer",
        type: "circle",
        source: "points"
    });
});

mapB остаётся полностью независимой и требует отдельного определения источников.


Ограничения WebGL-контекстов

Браузеры ограничивают количество одновременно активных WebGL-контекстов. При создании множества карт возможно достижение лимита, что приводит к сбросу контекста и ошибкам рендеринга.

Факторы, влияющие на стабильность:

  • количество одновременно активных карт
  • сложность стилей (количество слоёв, фильтров, источников)
  • наличие анимаций и частых перерисовок

Оптимизационные подходы включают:

  • уничтожение неиспользуемых карт через remove()
  • временное скрытие контейнеров вместо пересоздания
  • уменьшение частоты обновлений синхронизации

Динамическое создание и уничтожение карт

При интерфейсах с вкладками или модальными окнами карты часто создаются и уничтожаются динамически:

let activeMap = null;

function createMap(container) {
    activeMap = new maplibregl.Map({
        container,
        style: styleUrl,
        center: [0, 0],
        zoom: 2
    });
}

function destroyMap() {
    if (activeMap) {
        activeMap.remove();
        activeMap = null;
    }
}

Такой подход предотвращает накопление WebGL-контекстов и снижает нагрузку на GPU.


Общие источники синхронизации состояния

При наличии более двух карт целесообразно использовать централизованное состояние:

const state = {
    center: [0, 0],
    zoom: 3,
    bearing: 0
};

function updateState(map) {
    const c = map.getCenter();
    state.center = [c.lng, c.lat];
    state.zoom = map.getZoom();
    state.bearing = map.getBearing();
}

function applyState(map) {
    map.jumpTo(state);
}

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


Синхронизация с учётом производительности

При синхронизации нескольких карт важно учитывать частоту кадров WebGL. Использование requestAnimationFrame позволяет сгладить обновления:

let queued = false;

mapA.on("move", () => {
    if (!queued) {
        queued = true;

        requestAnimationFrame(() => {
            mapB.jumpTo({
                center: mapA.getCenter(),
                zoom: mapA.getZoom()
            });

            queued = false;
        });
    }
});

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