Loading states

Состояние загрузки (loading state) — это промежуточное состояние интерфейса, возникающее между моментом создания карты и моментом полной готовности данных к отображению. В приложениях, использующих Mapbox GL JS, загрузка может происходить на нескольких уровнях одновременно:

  • загрузка стиля карты;
  • загрузка тайлов;
  • загрузка векторных данных;
  • загрузка GeoJSON-источников;
  • загрузка изображений и иконок;
  • загрузка пользовательских данных через API;
  • перестроение слоёв после изменения источников.

Поскольку Mapbox GL JS активно использует асинхронные операции, грамотная работа с состояниями загрузки становится важной частью разработки пользовательского интерфейса.


Почему состояния загрузки важны

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

  • пустой экран;
  • неполностью отрисованная карта;
  • внезапное появление объектов;
  • ощущение зависания приложения;
  • повторные действия из-за отсутствия обратной связи.

Корректная реализация loading states позволяет:

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

Жизненный цикл загрузки карты

После создания объекта Map происходит несколько этапов:

  1. Инициализация экземпляра карты.
  2. Загрузка стиля.
  3. Получение тайлов.
  4. Загрузка источников данных.
  5. Создание слоёв.
  6. Отрисовка первого кадра.
  7. Переход в полностью загруженное состояние.

Пример создания карты:

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [37.6173, 55.7558],
    zoom: 10
});

Сразу после вызова конструктора карта ещё не готова к работе.


Событие load

Основным событием завершения начальной загрузки является load.

map.on('load', () => {
    console.log('Карта полностью загружена');
});

Событие возникает после:

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

Именно внутри обработчика load обычно добавляют:

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

Пример:

map.on('load', () => {
    map.addSource('cities', {
        type: 'geojson',
        data: '/data/cities.geojson'
    });

    map.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities'
    });
});

Проверка загрузки через loaded()

Mapbox GL JS предоставляет метод loaded().

if (map.loaded()) {
    console.log('Карта готова');
}

Метод возвращает:

true

если:

  • карта завершила загрузку;
  • отсутствуют активные операции загрузки ресурсов.

Использование:

function waitForMap() {
    if (map.loaded()) {
        initializeLayers();
    }
}

Событие idle

Событие idle считается одним из наиболее надёжных индикаторов полной готовности карты.

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

В отличие от load, событие idle означает:

  • завершение всех сетевых запросов;
  • отсутствие анимаций;
  • отсутствие ожидающих обновлений рендеринга.

Использование полезно при:

  • создании скриншотов карты;
  • экспорте изображений;
  • запуске тяжёлых вычислений.

Отображение индикатора загрузки

Простейший вариант — показать оверлей поверх карты.

HTML:

<div id="loader">
    Загрузка карты...
</div>

<div id="map"></div>

CSS:

#loader {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: white;
    z-index: 1000;
}

Jav * aScript:

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

map.on('load', () => {
    loader.style.display = 'none';
});

Пока карта загружается, пользователь видит сообщение о процессе загрузки.


Использование спиннера

Более распространённый вариант — анимированный индикатор.

HTML:

<div id="loader">
    <div class="spinner"></div>
</div>

CSS:

.spinner {
    width: 48px;
    height: 48px;
    border: 4px solid #ddd;
    border-top-color: #0066ff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

После завершения загрузки:

map.on('idle', () => {
    document.getElementById('loader').remove();
});

Отслеживание загрузки источников данных

Источник GeoJSON может загружаться отдельно от карты.

map.addSource('roads', {
    type: 'geojson',
    data: '/data/roads.geojson'
});

В этом случае необходимо учитывать загрузку самого источника.

Событие:

map.on('sourcedata', (event) => {
    console.log(event);
});

Содержит информацию о:

  • типе источника;
  • состоянии загрузки;
  • происхождении данных.

Проверка готовности источника

Событие sourcedata можно использовать для определения окончания загрузки.

map.on('sourcedata', (event) => {
    if (
        event.sourceId === 'roads' &&
        event.isSourceLoaded
    ) {
        console.log('Источник roads загружен');
    }
});

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


Отслеживание событий data

Событие data возникает при загрузке различных ресурсов карты.

map.on('data', (event) => {
    console.log(event.dataType);
});

Возможные значения:

style
source

Пример:

map.on('data', (event) => {
    if (event.dataType === 'source') {
        console.log('Получены данные источника');
    }
});

Использование isSourceLoaded

Объект события содержит свойство:

event.isSourceLoaded

Пример:

map.on('sourcedata', (event) => {
    if (event.isSourceLoaded) {
        console.log('Источник полностью готов');
    }
});

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


Подсчёт загружаемых источников

В сложных приложениях может использоваться множество источников.

Например:

const sources = [
    'roads',
    'buildings',
    'districts',
    'stations'
];

Создаётся счётчик:

const loadedSources = new Set();

Отслеживание:

map.on('sourcedata', (event) => {
    if (
        event.isSourceLoaded &&
        sources.includes(event.sourceId)
    ) {
        loadedSources.add(event.sourceId);
    }

    if (loadedSources.size === sources.length) {
        console.log('Все источники загружены');
    }
});

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


Скелетон-экраны вместо спиннеров

Современные интерфейсы часто используют Skeleton UI.

Вместо:

Загрузка...

отображается упрощённая структура будущего интерфейса.

Пример:

<div class="map-skeleton"></div>
.map-skeleton {
    width: 100%;
    height: 600px;
    background: linear-gradient(
        90deg,
        #eeeeee,
        #f5f5f5,
        #eeeeee
    );

    background-size: 200% 100%;

    animation: shimmer 1.5s infinite;
}

После готовности карты:

map.on('idle', () => {
    skeleton.remove();
});

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


Асинхронная загрузка GeoJSON

Часто данные загружаются через fetch.

const response = await fetch('/data/objects.geojson');

const data = await response.json();

На период ожидания отображается индикатор.

showLoader();

После получения данных:

map.addSource('objects', {
    type: 'geojson',
    data
});

hideLoader();

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

Любая загрузка должна сопровождаться обработкой ошибок.

Пример:

try {
    const response = await fetch(url);

    const data = await response.json();

    updateMap(data);
}
catch(error) {
    showError(error);
}

Без этого пользователь может бесконечно наблюдать индикатор загрузки.


Таймаут загрузки

Иногда сервер отвечает слишком долго.

Решение:

const controller = new AbortController();

setTimeout(() => {
    controller.abort();
}, 10000);

Запрос:

await fetch(url, {
    signal: controller.signal
});

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

Не удалось загрузить данные

Индикатор прогресса загрузки нескольких наборов данных

Предположим, требуется загрузить пять файлов.

const files = [
    '/a.geojson',
    '/b.geojson',
    '/c.geojson',
    '/d.geojson',
    '/e.geojson'
];

Счётчик:

let completed = 0;

После каждой загрузки:

completed++;

const progress =
    completed / files.length * 100;

Обновление интерфейса:

progressBar.style.width =
    `${progress}%`;

В результате пользователь видит реальный прогресс выполнения.


Состояния загрузки пользовательских изображений

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

map.loadImage('/images/marker.png',
    (error, image) => {

        if (error) {
            throw error;
        }

        map.addImage('marker', image);
    }
);

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


Асинхронная загрузка изображений через Promise

Удобный подход:

function loadImage(url) {
    return new Promise((resolve, reject) => {
        map.loadImage(url, (error, image) => {
            if (error) {
                reject(error);
                return;
            }

            resolve(image);
        });
    });
}

Использование:

const image =
    await loadImage('/marker.png');

map.addImage('marker', image);

После завершения загрузки можно скрыть индикатор.


Отслеживание активности карты

Во время перемещения карта может подгружать новые тайлы.

Проверка:

map.isMoving();

Пример:

if (map.isMoving()) {
    console.log('Карта перемещается');
}

Это полезно для отображения фонового состояния загрузки.


Индикатор фоновой загрузки тайлов

При изменении масштаба могут загружаться новые данные.

Небольшой индикатор в углу интерфейса помогает сообщить о процессе.

map.on('dataloading', () => {
    loader.classList.add('visible');
});

map.on('idle', () => {
    loader.classList.remove('visible');
});

Подход широко используется в профессиональных GIS-системах.


Комбинирование нескольких состояний загрузки

В крупных приложениях обычно существуют несколько независимых состояний:

{
    mapLoaded: false,
    styleLoaded: false,
    geojsonLoaded: false,
    imagesLoaded: false,
    apiLoaded: false
}

Интерфейс становится доступным только после завершения всех операций.

Пример проверки:

function isReady() {
    return (
        state.mapLoaded &&
        state.styleLoaded &&
        state.geojsonLoaded &&
        state.imagesLoaded &&
        state.apiLoaded
    );
}

После получения результата:

if (isReady()) {
    hideLoader();
}

Централизованный менеджер загрузки

Для больших проектов удобно создавать отдельный объект управления загрузкой.

class LoadingManager {
    constructor() {
        this.tasks = new Set();
    }

    start(task) {
        this.tasks.add(task);
    }

    finish(task) {
        this.tasks.delete(task);
    }

    isLoading() {
        return this.tasks.size > 0;
    }
}

Использование:

manager.start('geojson');
manager.start('icons');

manager.finish('geojson');
manager.finish('icons');

Проверка:

if (!manager.isLoading()) {
    hideLoader();
}

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