Синхронизация в контексте Mapbox GL JS означает согласование состояния карты с внешними источниками данных: пользовательским интерфейсом, URL, хранилищем приложения или другими компонентами системы. Ключевые параметры состояния карты включают центр, масштаб, наклон, азимут (bearing), а также активные слои, фильтры и выделенные объекты.
Mapbox GL JS предоставляет событийную модель и набор методов API, позволяющих отслеживать изменения состояния карты и программно их воспроизводить. Базовая единица синхронизации — объект карты, через который проходят все изменения.
Основной механизм отслеживания изменений — события карты. При каждом изменении состояния генерируются события, которые можно использовать для обновления внешнего состояния.
Ключевые события:
move — любое изменение положения картыmoveend — завершение перемещенияzoom — изменение масштабаzoomend — завершение масштабированияrotate — изменение угла поворотаpitch — изменение наклонаrender — обновление кадра картыПример базового отслеживания состояния:
map.on('moveend', () => {
const center = map.getCenter();
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();
const state = {
center: [center.lng, center.lat],
zoom,
bearing,
pitch
};
console.log(state);
});
Этот подход формирует поток состояния, который может быть передан в любое внешнее хранилище.
Двусторонняя синхронизация предполагает, что:
Основная сложность заключается в предотвращении циклических обновлений.
Типовой подход — использование флага блокировки:
let isSyncing = false;
map.on('moveend', () => {
if (isSyncing) return;
const center = map.getCenter();
isSyncing = true;
externalState.set({
center: [center.lng, center.lat]
});
isSyncing = false;
});
При обратном обновлении:
externalState.subscribe((state) => {
if (isSyncing) return;
isSyncing = true;
map.jumpTo({
center: state.center,
zoom: state.zoom
});
isSyncing = false;
});
Наиболее частая задача — поддержание актуального состояния камеры карты.
Mapbox GL JS предоставляет два основных метода управления камерой:
jumpTo — мгновенное изменениеeaseTo — плавная анимацияmap.jumpTo({
center: [lng, lat],
zoom: 10,
bearing: 0,
pitch: 0
});
map.easeTo({
center: [lng, lat],
zoom: 10,
duration: 800
});
Плавная синхронизация чаще используется при изменении состояния через интерфейс, так как снижает резкость визуальных переходов.
URL может выступать источником истины для состояния карты, что позволяет сохранять и делиться текущим видом.
function updateHash() {
const center = map.getCenter();
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();
const hash = `${zoom.toFixed(2)}/${center.lat.toFixed(5)}/${center.lng.toFixed(5)}/${bearing.toFixed(2)}/${pitch.toFixed(2)}`;
window.location.hash = hash;
}
map.on('moveend', updateHash);
function parseHash() {
const hash = window.location.hash.replace('#', '');
const parts = hash.split('/');
if (parts.length !== 5) return null;
return {
zoom: Number(parts[0]),
lat: Number(parts[1]),
lng: Number(parts[2]),
bearing: Number(parts[3]),
pitch: Number(parts[4])
};
}
const state = parseHash();
if (state) {
map.setCenter([state.lng, state.lat]);
map.setZoom(state.zoom);
map.setBearing(state.bearing);
map.setPitch(state.pitch);
}
В архитектурах с состоянием (Redux, Zustand, Vuex) карта обычно рассматривается как view-layer компонент, синхронизируемый через store.
store.subscribe((state) => {
const camera = state.map;
map.jumpTo({
center: camera.center,
zoom: camera.zoom,
bearing: camera.bearing,
pitch: camera.pitch
});
});
Обновление store:
map.on('moveend', () => {
const center = map.getCenter();
store.dispatch({
type: 'MAP_UPDATE',
payload: {
center: [center.lng, center.lat],
zoom: map.getZoom()
}
});
});
Интерфейс часто включает элементы управления: слайдеры масштаба, поля координат, переключатели слоёв.
map.on('zoom', () => {
zoomSlider.value = map.getZoom();
});
zoomSlider.addEventListener('input', (e) => {
map.setZoom(Number(e.target.value));
});
Состояние карты включает не только камеру, но и визуальные слои.
function setLayerVisibility(layerId, visible) {
map.setLayoutProperty(
layerId,
'visibility',
visible ? 'visible' : 'none'
);
}
map.setFilter('points-layer', [
'==',
['get', 'type'],
currentType
]);
Обновление фильтра из состояния:
store.subscribe((state) => {
map.setFilter('points-layer', [
'==',
['get', 'type'],
state.filterType
]);
});
Частые события move и zoom могут приводить
к избыточным обновлениям внешнего состояния.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const syncState = debounce(() => {
const center = map.getCenter();
console.log(center);
}, 100);
map.on('move', syncState);
moveend вместо moveСобытие moveend уменьшает нагрузку, так как вызывается
один раз после завершения интерактивного действия.
При работе с несколькими экземплярами карты возникает задача зеркалирования состояния.
function syncMaps(source, target) {
let syncing = false;
source.on('move', () => {
if (syncing) return;
syncing = true;
target.jumpTo({
center: source.getCenter(),
zoom: source.getZoom(),
bearing: source.getBearing(),
pitch: source.getPitch()
});
syncing = false;
});
}
Такой подход применяется в сравнительных интерфейсах и режимах split-view.
Маркерные объекты могут быть частью синхронизируемого состояния.
const marker = new mapboxgl.Marker()
.setLngLat([lng, lat])
.addTo(map);
Обновление маркера из состояния:
store.subscribe((state) => {
marker.setLngLat(state.selectedPoint);
});
Обратная синхронизация:
marker.getElement().addEventListener('click', () => {
store.dispatch({
type: 'SELECT_POINT',
payload: marker.getLngLat()
});
});
queryRenderedFeaturesИнтерактивные карты часто требуют синхронизации выбранных объектов.
map.on('click', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['points-layer']
});
if (features.length) {
const feature = features[0];
store.dispatch({
type: 'SELECT_FEATURE',
payload: feature.properties
});
}
});
При сложной синхронизации важно выделять единый источник истины. Распространённые архитектуры:
На практике предпочтительно избегать прямых связей «карта ↔︎ UI» без промежуточного слоя, чтобы исключить расхождение состояний.
При использовании анимаций важно учитывать их асинхронность.
map.easeTo({
center: [lng, lat],
zoom: 12,
duration: 1000
});
map.once('moveend', () => {
console.log('Переход завершён');
});
Событие moveend используется как точка фиксации
состояния после анимации.
В React-подобных архитектурах карта обычно интегрируется через эффект жизненного цикла.
useEffect(() => {
map.on('moveend', sync);
return () => {
map.off('moveend', sync);
};
}, []);
Ключевая особенность — обязательная очистка подписок, предотвращающая утечки состояния.
При подключении realtime-источников (WebSocket, SSE) карта становится визуализационным слоем.
socket.on('update', (data) => {
map.getSource('points').setData(data);
});
Обратная синхронизация:
map.on('moveend', () => {
socket.emit('viewport', {
bounds: map.getBounds().toArray()
});
});
Смена стиля карты сбрасывает источники и слои, поэтому синхронизация должна учитывать событие загрузки:
map.on('styledata', () => {
if (map.isStyleLoaded()) {
restoreLayersFromState();
}
});
При наличии нескольких источников (URL, store, UI) вводится приоритет:
Такое разделение позволяет стабилизировать поведение карты при загрузке и взаимодействии.
При потоковых данных и постоянных изменениях камеры используется сглаживание:
let lastUpdate = 0;
map.on('move', () => {
const now = Date.now();
if (now - lastUpdate < 50) return;
lastUpdate = now;
externalSync(map.getCenter());
});