Состояние загрузки (loading state) — это промежуточное состояние интерфейса, возникающее между моментом создания карты и моментом полной готовности данных к отображению. В приложениях, использующих Mapbox GL JS, загрузка может происходить на нескольких уровнях одновременно:
Поскольку Mapbox GL JS активно использует асинхронные операции, грамотная работа с состояниями загрузки становится важной частью разработки пользовательского интерфейса.
При отсутствии индикации загрузки пользователь сталкивается с несколькими проблемами:
Корректная реализация loading states позволяет:
После создания объекта Map происходит несколько
этапов:
Пример создания карты:
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [37.6173, 55.7558],
zoom: 10
});
Сразу после вызова конструктора карта ещё не готова к работе.
Основным событием завершения начальной загрузки является
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'
});
});
Mapbox GL JS предоставляет метод loaded().
if (map.loaded()) {
console.log('Карта готова');
}
Метод возвращает:
true
если:
Использование:
function waitForMap() {
if (map.loaded()) {
initializeLayers();
}
}
Событие 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 возникает при загрузке различных ресурсов
карты.
map.on('data', (event) => {
console.log(event.dataType);
});
Возможные значения:
style
source
Пример:
map.on('data', (event) => {
if (event.dataType === 'source') {
console.log('Получены данные источника');
}
});
Объект события содержит свойство:
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();
});
Такой подход визуально воспринимается быстрее.
Часто данные загружаются через 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);
}
);
До завершения загрузки изображение нельзя использовать в слоях.
Удобный подход:
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 и позволяет строить масштабируемые интерфейсы с предсказуемым поведением и корректной обратной связью для пользователя.