Сохранение слоев при смене стиля

В Mapbox GL JS стиль карты представляет собой полностью описанную визуальную конфигурацию: набор источников данных (sources), слоёв (layers), спрайтов, шрифтов и правил отрисовки. При вызове map.setStyle() происходит полная замена текущего стиля новым JSON-описанием.

Ключевой момент: все пользовательские слои и источники, добавленные через addLayer и addSource, удаляются при смене стиля, если не предусмотрена их повторная инициализация.

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


Жизненный цикл стиля и точка разрушения слоёв

При смене стиля последовательность событий выглядит следующим образом:

  1. Инициализируется загрузка нового style JSON
  2. Уничтожаются текущие WebGL-ресурсы, связанные со старым стилем
  3. Очищаются источники и слои
  4. Загружаются новые sources и layers из нового стиля
  5. Срабатывает событие styledata
  6. Срабатывает событие style.load (в зависимости от версии API)

Именно на этапе 3 происходит потеря пользовательских слоёв.


Базовая проблема сохранения слоёв

При наличии пользовательских визуализаций (heatmap, clusters, route layers, overlays) возникает типичная проблема:

  • после setStyle() они исчезают
  • повторное добавление требует синхронизации с моментом загрузки стиля
  • порядок слоёв часто нарушается

Эта проблема усиливается при частых переключениях стилей (например, светлый/тёмный режим или разные тематические карты).


Хранение конфигурации слоёв вне стиля

Практика устойчивого восстановления слоёв начинается с отделения описания слоёв от самого Mapbox-стиля.

Обычно используется структура:

  • список источников данных
  • список пользовательских слоёв
  • метаданные порядка вставки (beforeId)
const customLayers = [
  {
    id: 'route-line',
    type: 'line',
    source: 'route-source',
    paint: {
      'line-color': '#ff0000',
      'line-width': 4
    }
  },
  {
    id: 'stations',
    type: 'circle',
    source: 'stations-source',
    paint: {
      'circle-radius': 6,
      'circle-color': '#0066ff'
    }
  }
];

Восстановление слоёв через событие styledata

Наиболее устойчивый механизм — восстановление состояния после полной загрузки стиля.

Событие styledata срабатывает многократно, поэтому требуется фильтрация по состоянию загрузки:

map.on('styledata', () => {
  if (!map.isStyleLoaded()) return;

  restoreSources();
  restoreLayers();
});

Функция восстановления источников:

function restoreSources() {
  const existingSources = map.getStyle().sources;

  if (!existingSources['route-source']) {
    map.addSource('route-source', {
      type: 'geojson',
      data: routeGeojson
    });
  }

  if (!existingSources['stations-source']) {
    map.addSource('stations-source', {
      type: 'geojson',
      data: stationsGeojson
    });
  }
}

Функция восстановления слоёв:

function restoreLayers() {
  const style = map.getStyle();
  const existingLayers = style.layers.map(l => l.id);

  customLayers.forEach(layer => {
    if (existingLayers.includes(layer.id)) return;

    map.addLayer(layer);
  });
}

Контроль порядка слоёв (beforeId)

В Mapbox GL JS порядок отрисовки определяется порядком слоёв в стиле. При восстановлении важно учитывать контекст базового стиля.

map.addLayer(layer, 'waterway-label');

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


Проблема несоответствия базовых слоёв

Разные стили Mapbox (streets, dark, satellite) содержат разные наборы внутренних слоёв:

  • отсутствуют идентичные layer.id
  • различается структура label-слоёв
  • меняется порядок symbol layers

Поэтому фиксированный beforeId часто становится невалидным.

Решение — динамический поиск подходящей позиции:

function getLabelLayerId() {
  const layers = map.getStyle().layers;
  const labelLayer = layers.find(
    l => l.type === 'symbol' && l.layout && l.layout['text-field']
  );
  return labelLayer ? labelLayer.id : undefined;
}

Полное восстановление после setStyle

Наиболее надёжная схема строится вокруг явного контроля смены стиля.

function changeStyle(styleUrl) {
  map.setStyle(styleUrl);

  map.once('style.load', () => {
    restoreSources();
    restoreLayers();
  });
}

Альтернативный вариант с защитой от гонок:

map.on('style.load', () => {
  requestAnimationFrame(() => {
    restoreSources();
    restoreLayers();
  });
});

Кэширование состояния карты

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

const mapState = {
  sources: {},
  layers: []
};

При добавлении объектов:

mapState.sources['route-source'] = {
  type: 'geojson',
  data: routeGeojson
};

mapState.layers.push({
  id: 'route-line',
  type: 'line',
  source: 'route-source',
  paint: {
    'line-color': '#ff0000'
  }
});

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


Использование setStyle с сохранением пользовательских данных

Хотя setStyle() не сохраняет пользовательские слои, он может использоваться как триггер пересборки интерфейса.

const currentState = {
  sources: JSON.parse(JSON.stringify(mapState.sources)),
  layers: JSON.parse(JSON.stringify(mapState.layers))
};

map.setStyle(newStyle);

map.once('style.load', () => {
  Object.entries(currentState.sources).forEach(([id, source]) => {
    map.addSource(id, source);
  });

  currentState.layers.forEach(layer => {
    map.addLayer(layer);
  });
});

Особенности работы с растровыми и векторными источниками

Разные типы источников требуют разной стратегии восстановления:

GeoJSON

  • можно безопасно пересоздавать
  • данные обычно хранятся локально

Vector tiles

  • требуется повторное указание URL tileset
  • важно учитывать кэширование

Raster sources

  • зависят от стиля тайлового сервера
  • чувствительны к параметрам tileSize

Типовые ошибки при восстановлении слоёв

Добавление до полной загрузки стиля

map.addLayer(layer); // ошибка, если стиль не загружен

Корректно:

if (map.isStyleLoaded()) {
  map.addLayer(layer);
}

Дублирование слоёв

При повторном вызове восстановления без проверки существования возникает ошибка:

Error: Layer with id already exists

Решение — проверка через getStyle().layers.


Потеря порядка слоёв

Без указания beforeId пользовательские слои оказываются поверх всех элементов или под всеми элементами, нарушая визуальную структуру.


Архитектурный подход к устойчивым слоям

В сложных приложениях используется разделение:

  • Style Manager (смена темы карты)
  • Layer Registry (описания слоёв)
  • Source Registry (описания данных)
  • Renderer Controller (синхронизация)

Такой подход позволяет воспринимать Mapbox GL JS как реактивную систему, где стиль — лишь контейнер для отрисовки, а не источник состояния.


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

При частой смене стилей важно учитывать:

  • стиль загружается асинхронно
  • события могут накладываться
  • старые слои могут быть уничтожены до завершения добавления новых

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

let styleSwitching = false;

function safeSetStyle(style) {
  if (styleSwitching) return;

  styleSwitching = true;

  map.setStyle(style);

  map.once('style.load', () => {
    restoreLayers();
    styleSwitching = false;
  });
}