Синхронизация карт в MapLibre GL JS используется для построения интерфейсов, где несколько карт должны отражать одно и то же состояние или его производные: сравнение слоёв, параллельный анализ данных, «до/после» визуализации, синхронные панели обзора и детализации.
Базовая идея заключается в том, что состояние одной карты (центр, масштаб, поворот, наклон) становится источником изменений для других экземпляров, при этом исключаются циклические обновления и лишние перерисовки.
Ключевые параметры синхронизации:
center — географический центрzoom — уровень масштабаbearing — поворот картыpitch — наклон камерыСинхронизация строится вокруг событий изменения состояния карты:
move — изменение центраzoom — изменение масштабаrotate — изменение поворотаpitch — изменение наклонаmoveend — завершение взаимодействия пользователяrender — каждый кадр рендераНаиболее важные для синхронизации — move и
moveend, поскольку они отражают интерактивные изменения
камеры.
map.on('move', syncHandler);
map.on('zoom', syncHandler);
map.on('rotate', syncHandler);
map.on('pitch', syncHandler);
Наиболее устойчивый подход — назначение одной карты источником истины.
const master = new maplibregl.Map({
container: 'map1',
style: 'https://demotiles.maplibre.org/style.json',
center: [30, 50],
zoom: 4
});
const slave = new maplibregl.Map({
container: 'map2',
style: 'https://demotiles.maplibre.org/style.json',
center: [30, 50],
zoom: 4
});
function syncFromMaster() {
const center = master.getCenter();
const zoom = master.getZoom();
const bearing = master.getBearing();
const pitch = master.getPitch();
slave.jumpTo({ center, zoom, bearing, pitch });
}
master.on('move', syncFromMaster);
master.on('zoom', syncFromMaster);
master.on('rotate', syncFromMaster);
master.on('pitch', syncFromMaster);
Особенность jumpTo — мгновенное применение состояния без
анимации, что снижает риск расхождений и дрожания интерфейса.
При двусторонней синхронизации возникает эффект бесконечного цикла:
Решение — флаг блокировки синхронизации.
let isSyncing = false;
function sync(mapFrom, mapTo) {
if (isSyncing) return;
isSyncing = true;
const state = {
center: mapFrom.getCenter(),
zoom: mapFrom.getZoom(),
bearing: mapFrom.getBearing(),
pitch: mapFrom.getPitch()
};
mapTo.jumpTo(state);
isSyncing = false;
}
Более устойчивый вариант — локальная блокировка на уровне каждой карты:
function createSyncPair(mapA, mapB) {
let lockA = false;
let lockB = false;
mapA.on('move', () => {
if (lockA) return;
lockB = true;
mapB.jumpTo(mapA.getCenter());
lockB = false;
});
mapB.on('move', () => {
if (lockB) return;
lockA = true;
mapA.jumpTo(mapB.getCenter());
lockA = false;
});
}
requestAnimationFrame для оптимизацииПри высокой частоте событий (move генерируется на каждый
кадр) прямое обновление нескольких карт приводит к избыточным
пересчётам.
Оптимизация — батчинг обновлений:
let pending = false;
function scheduleSync(source, targets) {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
const state = {
center: source.getCenter(),
zoom: source.getZoom(),
bearing: source.getBearing(),
pitch: source.getPitch()
};
targets.forEach(map => map.jumpTo(state));
pending = false;
});
}
Этот подход снижает нагрузку на GPU и предотвращает «дёрганье» интерфейса при перетаскивании.
Иногда требуется не полная идентичность карт, а сохранение относительного сдвига между ними.
Пусть:
const offsetCenter = (center, dx, dy) => {
return [center[0] + dx, center[1] + dy];
};
mapA.on('move', () => {
const c = mapA.getCenter();
mapB.jumpTo({
center: offsetCenter(c, 0.5, 0.2),
zoom: mapA.getZoom(),
bearing: mapA.getBearing()
});
});
Такой подход используется в интерфейсах сравнения спутниковых снимков или слоёв разных лет.
Синхронизация не должна ограничиваться географическими координатами. Важно учитывать проекцию и состояние камеры.
MapLibre GL JS использует Web Mercator, поэтому:
center в разных масштабах могут визуально
расходитьсяbearing влияет на ориентацию экранных координатpitch изменяет перспективное искажениеКорректная синхронизация требует полного копирования камеры:
function syncCamera(from, to) {
to.jumpTo({
center: from.getCenter(),
zoom: from.getZoom(),
bearing: from.getBearing(),
pitch: from.getPitch()
});
}
В более сложных интерфейсах обе карты могут быть активными. Тогда вводится приоритет последнего действия:
let lastActive = null;
function attachSync(a, b) {
a.on('move', () => {
lastActive = a;
if (lastActive === a) {
b.jumpTo(a.getCenter());
}
});
b.on('move', () => {
lastActive = b;
if (lastActive === b) {
a.jumpTo(b.getCenter());
}
});
}
Такой подход предотвращает конфликт одновременного управления.
Координация карт часто включает не только камеру, но и визуальные слои:
setLayoutProperty)setPaintProperty)setData)function syncLayers(sourceMap, targetMap) {
const layers = sourceMap.getStyle().layers;
layers.forEach(layer => {
const visibility = sourceMap.getLayoutProperty(layer.id, 'visibility');
targetMap.setLayoutProperty(layer.id, 'visibility', visibility);
});
}
При масштабировании системы предпочтительнее использовать внешнее хранилище состояния:
const state = {
center: [0, 0],
zoom: 3
};
function updateState(newState) {
Object.assign(state, newState);
emit('stateChanged', state);
}
map.on('move', () => {
updateState({
center: map.getCenter(),
zoom: map.getZoom()
});
});
on('stateChanged', (s) => {
map.jumpTo(s);
});
Это позволяет синхронизировать неограниченное количество карт без прямых связей между ними.
Со временем две карты могут расходиться из-за:
Решение — периодическая «жёсткая» коррекция:
setInterval(() => {
const a = mapA.getCenter();
const b = mapB.getCenter();
if (distance(a, b) > 0.0001) {
mapB.jumpTo(mapA.getCenter());
}
}, 2000);
При синхронизации нескольких карт важно учитывать:
easeTo на предсказуемость
состоянияЧасто используется комбинация:
jumpTo для синхронизацииeaseTo только для пользовательской картыСложные интерфейсы требуют различения источника действия:
map.on('movestart', () => {
map.isUserInteracting = true;
});
map.on('moveend', () => {
map.isUserInteracting = false;
});
И синхронизация выполняется только при активном пользовательском управлении:
if (map.isUserInteracting) {
sync(map, otherMap);
}
Так предотвращается реакция на программные изменения как на пользовательские.