Генерация спрайтов

Спрайт (sprite) представляет собой единый графический атлас, содержащий набор небольших изображений, используемых картой в качестве иконок, маркеров, пиктограмм, обозначений объектов и других графических элементов. Вместо загрузки множества отдельных файлов браузер получает один общий набор изображений и данные о расположении каждого изображения внутри этого набора.

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

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

В MapLibre GL JS спрайты чаще всего используются слоями типа symbol, где изображения отображаются через свойство icon-image.


Структура спрайта

Спрайт состоит из двух файлов:

  1. PNG-файл с объединёнными изображениями.
  2. JSON-файл с описанием координат каждого изображения внутри PNG.

Пример структуры:

sprite.png
sprite.json

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

{
  "restaurant": {
    "x": 0,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  },
  "hotel": {
    "x": 32,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  }
}

Каждый объект описывает отдельную иконку:

  • x, y — координаты внутри атласа;
  • width, height — размеры изображения;
  • pixelRatio — коэффициент плотности пикселей.

Подключение спрайта в стиле карты

Спрайт задаётся через свойство sprite внутри объекта стиля.

Пример:

const map = new maplibregl.Map({
    container: 'map',
    style: {
        version: 8,
        sprite: 'https://example.com/sprites/sprite',
        sources: {},
        layers: []
    }
});

Важно понимать, что расширение файла не указывается.

Если задан путь:

sprite: 'https://example.com/sprites/sprite'

MapLibre автоматически попытается загрузить:

https://example.com/sprites/sprite.json
https://example.com/sprites/sprite.png

Для экранов высокой плотности дополнительно могут использоваться файлы:

sprite@2x.json
sprite@2x.png

Использование спрайтов в слоях Symbol

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

Пример слоя:

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

Значение:

'restaurant'

должно соответствовать ключу в файле спрайта:

{
  "restaurant": {
    ...
  }
}

Если иконка отсутствует, объект на карте не будет отображён.


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

Свойство icon-image поддерживает выражения.

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

Пример GeoJSON:

{
  "type": "Feature",
  "properties": {
    "category": "hotel"
  },
  "geometry": {
    "type": "Point",
    "coordinates": [30, 50]
  }
}

Настройка слоя:

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

В результате значение свойства:

"hotel"

автоматически приведёт к использованию иконки:

{
  "hotel": { ... }
}

из спрайта.

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


Генерация спрайтов вручную

Самый простой способ создания спрайта заключается в объединении нескольких PNG-файлов в единый атлас и формировании JSON-описания.

Допустим, имеются изображения:

restaurant.png
hotel.png
airport.png
hospital.png

После упаковки получается:

sprite.png
sprite.json

JSON:

{
  "restaurant": {
    "x": 0,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  },
  "hotel": {
    "x": 32,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  },
  "airport": {
    "x": 64,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  },
  "hospital": {
    "x": 96,
    "y": 0,
    "width": 32,
    "height": 32,
    "pixelRatio": 1
  }
}

После этого набор становится готовым для использования в MapLibre GL JS.


Использование утилиты spritezero

На практике спрайты редко создаются вручную. Обычно применяется утилита Spritezero.

Установка:

npm install -g @mapbox/spritezero-cli

Исходная структура:

icons/
├── restaurant.svg
├── hotel.svg
├── airport.svg
└── hospital.svg

Генерация:

spritezero sprite icons

Результат:

sprite.png
sprite.json

Для Retina-дисплеев:

spritezero --retina sprite icons

Будут созданы:

sprite@2x.png
sprite@2x.json

MapLibre сможет автоматически использовать соответствующий вариант.


Использование SVG-иконок

SVG является наиболее распространённым форматом для подготовки спрайтов.

Преимущества SVG:

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

Пример SVG:

<svg width="32" height="32"
     xmlns="http://www.w3.org/2000/svg">
    <circle
        cx="16"
        cy="16"
        r="12"
        fill="#ff0000" />
</svg>

Набор SVG-файлов удобно использовать как исходный материал для автоматической генерации спрайта.


Retina-спрайты

Современные устройства часто имеют коэффициент масштабирования 2x или выше.

Если использовать обычный спрайт:

sprite.png

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

Для решения создаются файлы:

sprite@2x.png
sprite@2x.json

Пример записи:

{
  "restaurant": {
    "x": 0,
    "y": 0,
    "width": 64,
    "height": 64,
    "pixelRatio": 2
  }
}

MapLibre определяет плотность экрана автоматически и выбирает подходящий вариант.


Загрузка изображений без спрайта

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

Пример:

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

        if (error) {
            throw error;
        }

        map.addImage('restaurant', image);

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

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

Подход подходит для:

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

Генерация изображений через Canvas

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

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

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

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

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

ctx.fillStyle = '#0080ff';
ctx.beginPath();
ctx.arc(32, 32, 20, 0, Math.PI * 2);
ctx.fill();

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

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

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

layout: {
    'icon-image': 'generated-marker'
}

Такой механизм полезен для генерации графики в реальном времени.


Реакция на отсутствие изображения

Иногда данные содержат ссылки на иконки, которых ещё нет в карте.

MapLibre генерирует событие:

styleimagemissing

Пример обработки:

map.on(
    'styleimagemissing',
    (event) => {

        const id = event.id;

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

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

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

        ctx.fillStyle = '#ff0000';

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

        const imageData =
            ctx.getImageData(
                0,
                0,
                64,
                64
            );

        map.addImage(id, {
            width: 64,
            height: 64,
            data: imageData.data
        });
    }
);

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


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

Для изменяемых изображений используется метод:

map.updateImage()

Пример:

map.updateImage(
    'vehicle',
    newImage
);

Это позволяет обновлять внешний вид объектов без пересоздания слоя.

Типичные сценарии:

  • отображение статуса оборудования;
  • изменение цвета маркеров;
  • анимация объектов;
  • визуализация телеметрии.

Анимированные изображения

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

Пример объекта изображения:

const animatedImage = {
    width: 64,
    height: 64,
    data: new Uint8Array(64 * 64 * 4),

    onAdd() {
        this.canvas =
            document.createElement('canvas');

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

        this.context =
            this.canvas.getContext('2d');
    },

    render() {

        const time = Date.now();

        const radius =
            10 + Math.sin(time / 300) * 5;

        this.context.clearRect(
            0,
            0,
            64,
            64
        );

        this.context.beginPath();

        this.context.arc(
            32,
            32,
            radius,
            0,
            Math.PI * 2
        );

        this.context.fill();

        this.data =
            this.context.getImageData(
                0,
                0,
                64,
                64
            ).data;

        return true;
    }
};

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

map.addImage(
    'pulse',
    animatedImage
);

После этого иконка будет непрерывно перерисовываться.


Организация крупных наборов спрайтов

При работе с большими картографическими проектами количество иконок может исчисляться сотнями.

Распространённая структура:

icons/
├── transport/
├── tourism/
├── services/
├── emergency/
├── shopping/
└── administration/

Имена рекомендуется делать уникальными:

transport-bus
transport-train
transport-airport

tourism-hotel
tourism-museum
tourism-camping

Это исключает конфликты при расширении проекта.


Ограничения и особенности

При работе со спрайтами необходимо учитывать несколько факторов:

  • чрезмерно большой атлас увеличивает потребление памяти;
  • большое количество изображений повышает время загрузки стиля;
  • Retina-версии требуют дополнительного объёма ресурсов;
  • удаление изображения через API не освобождает память мгновенно;
  • размеры иконок лучше унифицировать для упрощения поддержки.

Для большинства проектов эффективной считается стратегия использования единого базового спрайта и динамической загрузки редких изображений через addImage().


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

Для статических картографических стилей

  • использовать единый спрайт;
  • генерировать его автоматически;
  • применять SVG в качестве исходников;
  • поддерживать Retina-вариант.

Для интерактивных приложений

  • сочетать спрайты и addImage();
  • применять styleimagemissing;
  • использовать Canvas для динамической графики.

Для высоконагруженных карт

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

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