Индикаторы загрузки

Модель загрузки данных в Leaflet

Карты в Leaflet строятся вокруг асинхронной загрузки тайлов, векторных данных и дополнительных слоёв. Основной источник задержек — сетевые запросы к тайловым серверам. При этом библиотека предоставляет событийную модель, позволяющую отслеживать состояние загрузки на уровне карты и отдельных слоёв.

Ключевые особенности:

  • загрузка тайлов выполняется параллельно;
  • состояние карты не блокируется при ожидании ресурсов;
  • события распространяются от слоёв к карте;
  • отсутствие встроенного UI для индикаторов загрузки.

Это создаёт основу для построения собственных индикаторов.


События карты, связанные с загрузкой

Leaflet предоставляет два базовых события уровня карты:

  • loading — начинается процесс загрузки хотя бы одного слоя;
  • load — все активные загрузки завершены.
map.on('loading', () => {
  console.log('Начало загрузки данных карты');
});

map.on('load', () => {
  console.log('Все данные загружены');
});

Особенность этих событий заключается в том, что они агрегируют состояние всех подключённых слоёв, включая TileLayer, ImageOverlay, GeoJSON и другие асинхронные источники.


События тайлового слоя

Наиболее важный источник индикаторов — TileLayer.

Основные события:

  • loading — старт загрузки тайлов;
  • load — завершение загрузки всех тайлов текущего видимого набора;
  • tileloadstart — начало загрузки отдельного тайла;
  • tileload — успешная загрузка тайла;
  • tileerror — ошибка загрузки тайла.
const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');

tileLayer.on('loading', () => {
  console.log('Загрузка тайлов началась');
});

tileLayer.on('tileload', (e) => {
  console.log('Тайл загружен:', e.tile.src);
});

tileLayer.on('tileerror', (e) => {
  console.log('Ошибка загрузки тайла:', e.tile.src);
});

tileLayer.on('load', () => {
  console.log('Все тайлы загружены');
});

Эти события позволяют строить как простые индикаторы активности, так и детализированные прогресс-бары.


Базовый индикатор загрузки (overlay spinner)

Самый распространённый подход — отображение слоя-перекрытия с индикатором.

HTML-разметка

<div id="map"></div>
<div id="loader" class="loader hidden"></div>

CSS

#map {
  height: 100vh;
}

.loader {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  border: 4px solid #ccc;
  border-top-color: #1a73e8;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  transform: translate(-50%, -50%);
  z-index: 1000;
}

.hidden {
  display: none;
}

@keyframes spin {
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}

JavaScript

const loader = document.getElementById('loader');

function showLoader() {
  loader.classList.remove('hidden');
}

function hideLoader() {
  loader.classList.add('hidden');
}

map.on('loading', showLoader);
map.on('load', hideLoader);

Такой подход отражает только общее состояние загрузки без детализации.


Подсчёт активных тайлов

Для более точного контроля используется счётчик активных загрузок.

let activeTiles = 0;

function updateLoader() {
  if (activeTiles > 0) {
    showLoader();
  } else {
    hideLoader();
  }
}

tileLayer.on('tileloadstart', () => {
  activeTiles++;
  updateLoader();
});

tileLayer.on('tileload', () => {
  activeTiles--;
  updateLoader();
});

tileLayer.on('tileerror', () => {
  activeTiles--;
  updateLoader();
});

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


Прогресс-бар загрузки тайлов

Leaflet не предоставляет процент загрузки, но его можно вычислить относительно ожидаемого количества тайлов.

Принцип работы

  1. оценка количества тайлов в текущем viewport;
  2. отслеживание загруженных тайлов;
  3. вычисление отношения.

Реализация

let totalTiles = 0;
let loadedTiles = 0;

function updateProgress() {
  const percent = totalTiles === 0 ? 0 : (loadedTiles / totalTiles) * 100;
  console.log(`Загрузка: ${percent.toFixed(0)}%`);
}

tileLayer.on('loading', () => {
  totalTiles = 0;
  loadedTiles = 0;
});

tileLayer.on('tileloadstart', () => {
  totalTiles++;
});

tileLayer.on('tileload', () => {
  loadedTiles++;
  updateProgress();
});

tileLayer.on('tileerror', () => {
  loadedTiles++;
  updateProgress();
});

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


Индикатор загрузки на уровне слоя

Каждый слой может иметь собственную логику отображения состояния.

function attachLayerLoader(layer) {
  layer.on('loading', () => {
    console.log('Слой загружается');
  });

  layer.on('load', () => {
    console.log('Слой загружен');
  });
}

Это особенно важно при работе с несколькими слоями одновременно:

  • базовые тайлы;
  • векторные слои GeoJSON;
  • WMS/WMTS слои;
  • динамические оверлеи.

Интеграция с пользовательскими контролами

Leaflet позволяет создавать собственные UI-компоненты через L.Control.

const LoadingControl = L.Control.extend({
  onAdd: function () {
    const div = L.DomUtil.create('div', 'loading-control');
    div.innerHTML = '0%';
    return div;
  }
});

const loadingControl = new LoadingControl({ position: 'topright' });
map.addControl(loadingControl);

Обновление состояния:

function setLoadingText(text) {
  const el = document.querySelector('.loading-control');
  if (el) el.innerHTML = text;
}

Синхронизация нескольких источников данных

При наличии нескольких TileLayer или комбинированных источников требуется централизованный трекинг.

let loadingSources = 0;

function startLoading() {
  loadingSources++;
  showLoader();
}

function endLoading() {
  loadingSources--;
  if (loadingSources <= 0) {
    loadingSources = 0;
    hideLoader();
  }
}

function attachTracking(layer) {
  layer.on('loading', startLoading);
  layer.on('load', endLoading);
}

attachTracking(tileLayer1);
attachTracking(tileLayer2);

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


Обработка ошибок загрузки

Ошибки тайлов напрямую влияют на UX и должны учитываться в логике индикатора.

tileLayer.on('tileerror', (e) => {
  console.warn('Ошибка тайла:', e.tile.src);
});

Расширенный вариант с повторными попытками:

tileLayer.on('tileerror', (e) => {
  const img = e.tile;
  setTimeout(() => {
    img.src = img.src;
  }, 1000);
});

Оптимизация частоты обновлений UI

При массовой загрузке тайлов события могут генерироваться слишком часто. Для снижения нагрузки применяется requestAnimationFrame.

let pendingUpdate = false;

function scheduleUpdate() {
  if (!pendingUpdate) {
    pendingUpdate = true;

    requestAnimationFrame(() => {
      updateProgress();
      pendingUpdate = false;
    });
  }
}

Использование такого подхода снижает количество DOM-операций при интенсивной загрузке.


Комбинированные индикаторы состояния карты

На практике индикаторы объединяют несколько сигналов:

  • глобальное событие карты (loading/load);
  • счётчик активных тайлов;
  • прогресс-бар;
  • обработка ошибок.
map.on('loading', showLoader);
map.on('load', hideLoader);

tileLayer.on('tileloadstart', () => {
  activeTiles++;
  updateLoader();
});

tileLayer.on('tileload', () => {
  activeTiles--;
  updateLoader();
});

Такая комбинация обеспечивает согласованное отображение состояния независимо от источника данных.


Поведение при перемещении карты

Перемещение и масштабирование карты вызывает повторную загрузку тайлов. Это приводит к частым переходам состояния loading → load.

Особенность:

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

Управление индикатором при динамическом слое

При добавлении и удалении слоёв важно учитывать их жизненный цикл.

map.addLayer(tileLayer);

tileLayer.on('load', () => {
  console.log('Динамический слой загружен');
});

map.removeLayer(tileLayer);

Без корректного отслеживания возможно «зависание» индикатора из-за незавершённых событий.