Динамическое добавление изображений

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

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

Основными инструментами являются методы:

  • loadImage()
  • addImage()
  • updateImage()
  • removeImage()
  • hasImage()
  • listImages()

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


Принцип работы изображений в MapLibre

Процесс состоит из нескольких этапов:

  1. Получение изображения.
  2. Добавление изображения в менеджер ресурсов карты.
  3. Присвоение изображению уникального имени.
  4. Использование имени изображения в свойстве icon-image.

Схема выглядит следующим образом:

map.loadImage('/images/restaurant.png', (error, image) => {
    if (error) throw error;

    map.addImage('restaurant-icon', image);

    map.addLayer({
        id: 'restaurants',
        type: 'symbol',
        source: 'places',
        layout: {
            'icon-image': 'restaurant-icon'
        }
    });
});

После выполнения кода изображение становится частью стиля карты и может использоваться любым количеством слоёв.


Загрузка изображения через loadImage()

Метод loadImage() предназначен для загрузки изображения по URL.

Сигнатура:

map.loadImage(url, callback);

Параметры:

Параметр Описание
url Путь к изображению
callback Функция обратного вызова

Пример:

map.loadImage(
    'https://example.com/icons/hotel.png',
    (error, image) => {

        if (error) {
            console.error(error);
            return;
        }

        map.addImage('hotel-icon', image);
    }
);

Если загрузка завершилась успешно, объект изображения передаётся во второй аргумент callback-функции.


Добавление изображения через addImage()

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

Простейший вариант:

map.addImage('hotel-icon', image);

Здесь:

  • hotel-icon — внутренний идентификатор;
  • image — объект изображения.

После регистрации идентификатор становится доступным в стилях.

Пример использования:

layout: {
    'icon-image': 'hotel-icon'
}

Проверка существования изображения

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

Для проверки используется метод hasImage().

if (!map.hasImage('hotel-icon')) {
    map.addImage('hotel-icon', image);
}

Подобная проверка особенно полезна при работе с большим количеством динамически создаваемых слоёв.


Использование изображений в символических слоях

Наиболее распространённый сценарий — отображение изображений вместо стандартных маркеров.

Источник данных:

map.addSource('places', {
    type: 'geojson',
    data: {
        type: 'FeatureCollection',
        features: [
            {
                type: 'Feature',
                geometry: {
                    type: 'Point',
                    coordinates: [37.6176, 55.7558]
                }
            }
        ]
    }
});

Слой:

map.addLayer({
    id: 'places-layer',
    type: 'symbol',
    source: 'places',
    layout: {
        'icon-image': 'hotel-icon',
        'icon-size': 0.8
    }
});

Каждый объект будет отображён с использованием загруженной пиктограммы.


Масштабирование изображений

Размер иконки регулируется свойством icon-size.

layout: {
    'icon-image': 'hotel-icon',
    'icon-size': 1.5
}

Уменьшение:

'icon-size': 0.5

Увеличение:

'icon-size': 2

Размер изменяется относительно исходных размеров изображения.


Выбор изображения из атрибутов объекта

Одно из ключевых преимуществ MapLibre заключается в возможности выбирать изображение на основе данных объекта.

GeoJSON:

{
    type: 'Feature',
    properties: {
        icon: 'restaurant-icon'
    },
    geometry: {
        type: 'Point',
        coordinates: [37.62, 55.75]
    }
}

Слой:

map.addLayer({
    id: 'places',
    type: 'symbol',
    source: 'places',
    layout: {
        'icon-image': ['get', 'icon']
    }
});

В результате каждый объект может использовать собственную пиктограмму.


Регистрация нескольких изображений

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

const icons = [
    {
        id: 'hotel',
        url: '/icons/hotel.png'
    },
    {
        id: 'restaurant',
        url: '/icons/restaurant.png'
    },
    {
        id: 'museum',
        url: '/icons/museum.png'
    }
];

Загрузка:

icons.forEach(icon => {

    map.loadImage(icon.url, (error, image) => {

        if (error) return;

        map.addImage(icon.id, image);

    });

});

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

layout: {
    'icon-image': ['get', 'category']
}

Если значение поля category совпадает с идентификатором изображения, нужная иконка будет выбрана автоматически.


Использование выражения match

Для сложной логики выбора изображения применяется выражение match.

layout: {
    'icon-image': [
        'match',
        ['get', 'type'],

        'hotel',
        'hotel-icon',

        'restaurant',
        'restaurant-icon',

        'museum',
        'museum-icon',

        'default-icon'
    ]
}

Преимущества такого подхода:

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

Обновление изображения

Иногда требуется заменить изображение без удаления слоя.

Для этого используется метод updateImage().

map.updateImage(
    'vehicle-icon',
    newImage
);

Пример применения:

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

После обновления все объекты, использующие данное изображение, автоматически перерисовываются.


Удаление изображения

Для освобождения ресурсов используется метод removeImage().

map.removeImage('vehicle-icon');

Перед удалением желательно убедиться, что изображение больше не используется.

if (map.hasImage('vehicle-icon')) {
    map.removeImage('vehicle-icon');
}

Удалённое изображение становится недоступным для слоёв.


Получение списка зарегистрированных изображений

Для диагностики и отладки предусмотрен метод listImages().

const images = map.listImages();

console.log(images);

Результат:

[
    'hotel-icon',
    'restaurant-icon',
    'museum-icon'
]

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


Создание изображения через Canvas

Источник изображения не обязательно должен быть файлом.

MapLibre позволяет использовать содержимое элемента Canvas.

Создание холста:

const canvas = document.createElement('canvas');

canvas.width = 64;
canvas.height = 64;

const ctx = canvas.getContext('2d');

ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(32, 32, 30, 0, Math.PI * 2);
ctx.fill();

Добавление изображения:

map.addImage(
    'circle-icon',
    {
        width: 64,
        height: 64,
        data: ctx.getImageData(
            0,
            0,
            64,
            64
        ).data
    }
);

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


Генерация иконок на основе данных

Canvas часто используется для создания динамических значков.

Например, кругов разного цвета:

function createIcon(color) {

    const canvas = document.createElement('canvas');

    canvas.width = 64;
    canvas.height = 64;

    const ctx = canvas.getContext('2d');

    ctx.fillStyle = color;

    ctx.beginPath();
    ctx.arc(32, 32, 30, 0, Math.PI * 2);
    ctx.fill();

    return {
        width: 64,
        height: 64,
        data: ctx.getImageData(
            0,
            0,
            64,
            64
        ).data
    };

}

Регистрация:

map.addImage(
    'red-marker',
    createIcon('#ff0000')
);

map.addImage(
    'green-marker',
    createIcon('#00ff00')
);

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


Обработка события styleimagemissing

Иногда карта пытается отобразить изображение, которое ещё не зарегистрировано.

Для таких случаев существует событие styleimagemissing.

map.on(
    'styleimagemissing',
    e => {

        console.log(
            'Не найдено изображение:',
            e.id
        );

    }
);

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


Динамическое создание отсутствующих изображений

Наиболее мощный сценарий использования styleimagemissing — генерация иконок по требованию.

map.on(
    'styleimagemissing',
    e => {

        const canvas =
            document.createElement('canvas');

        canvas.width = 64;
        canvas.height = 64;

        const ctx =
            canvas.getContext('2d');

        ctx.fillStyle = '#0099ff';

        ctx.fillRect(
            0,
            0,
            64,
            64
        );

        map.addImage(
            e.id,
            {
                width: 64,
                height: 64,
                data: ctx.getImageData(
                    0,
                    0,
                    64,
                    64
                ).data
            }
        );

    }
);

Если слой запросит неизвестное изображение, оно будет создано автоматически.


Работа с изображениями высокой плотности пикселей

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

Например:

icon.png      32×32
icon@2x.png   64×64

Регистрация:

map.addImage(
    'icon',
    image,
    {
        pixelRatio: 2
    }
);

MapLibre корректно рассчитает масштаб и обеспечит более чёткое отображение на устройствах с высокой плотностью пикселей.


Использование SDF-изображений

SDF (Signed Distance Field) позволяет изменять цвет изображения средствами стиля.

Добавление:

map.addImage(
    'marker',
    image,
    {
        sdf: true
    }
);

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

Например:

paint: {
    'icon-color': '#ff0000'
}

Изменение цвета:

paint: {
    'icon-color': '#00ff00'
}

Один SDF-файл может использоваться для отображения множества цветовых вариантов.


Управление памятью при большом количестве изображений

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

Практические рекомендации:

  • удалять неиспользуемые изображения через removeImage();
  • проверять наличие через hasImage();
  • избегать повторной загрузки одинаковых ресурсов;
  • использовать генерацию через Canvas вместо хранения множества файлов;
  • применять SDF для цветовых вариаций одной иконки;
  • отслеживать содержимое через listImages().

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


Типичный сценарий динамической загрузки иконок

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

  1. Получение данных с сервера.
  2. Извлечение списка необходимых иконок.
  3. Проверка наличия через hasImage().
  4. Загрузка отсутствующих изображений через loadImage().
  5. Регистрация через addImage().
  6. Отображение объектов с использованием icon-image.
  7. Обновление через updateImage() при изменении состояния.
  8. Освобождение ресурсов через removeImage() после удаления данных.

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