Custom Icons
With symbol
layers, you can use custom icons to represent point features on the map. You can customize the icon's appearance per layer or per feature when using data-driven styling.
This guide will walk you through the various methods to using your own custom icons with MapsGL for vector symbol layers, specifically how to use them with the icon.image
paint property.
Using Sprites
Sprites (opens in a new tab), also known as sprite sheets, image sprites, or texture atlases, are a collection of images put into a single image. They are used to reduce the number of server requests when loading images and to improve the performance of rendering multiple images on the map.
The following is an example of a sprite consisting of multiple icons:
Generating a Sprite Sheet
Generating a sprite sheet is simple, and there are a number of tools available to help you create them. One popular command-line tool is spreet (opens in a new tab), which can be used to generate a sprite sheet from a directory of SVG images. It will then also generate a JSON index file that maps the image names to their positions in the sprite sheet which is required for loading the sprite sheet into MapsGL.
The following JSON index would be used to describe the icons and their layout in the sprite sheet above:
{
"icons": {
"partly-cloudy": {
"x": 0, "y": 0, "width": 256, "height": 256, "pixelRatio": 2
},
"rain": {
"x": 0, "y": 256, "width": 256, "height": 256, "pixelRatio": 2
},
"snow": {
"x": 256, "y": 0, "width": 256, "height": 256, "pixelRatio": 2
},
"storms": {
"x": 256, "y": 256, "width": 256, "height": 256, "pixelRatio": 2
},
"sunny": {
"x": 512, "y": 0, "width": 256, "height": 256, "pixelRatio": 2
}
}
}
Loading a Sprite Sheet
To load a sprite sheet into MapsGL, you will need to use the loadSprite
method on the MapController#style
instance. This method takes a unique identifier to prefix images in the sprite with and the URL of the JSON index that describes the icon layout in sprite sheet:
controller.style.loadSprite('my-sprite', 'https://example.com/sprite.json');
When loading sprite files, the sprite image is expected to be located at the same URL as the JSON index file but with the .png
extension. In this case, the sprite image would be located at https://example.com/sprite.png
and would be loaded automatically by MapsGL when the JSON index is loaded.
Once the required sprite files are loaded, each image in the sprite sheet will be cached by MapsGL and available to use in your styles. The loadSprite
method returns a Promise
that resolves when the sprite is loaded and ready to use, which you can then use to ensure that the sprite and its images are ready before using them in your layer paint styles.
Using Icons from a Sprite Sheet
To use an icon from a sprite sheet in your style, you will need to reference the icon name in the sprite sheet index file using the icon.image
property in the layer's paint styles. The icon name should be prefixed with the unique identifier you provided when loading the sprite sheet:
controller.addLayer('my-layer', {
type: 'symbol',
source: 'my-source',
paint: {
icon: {
image: 'my-sprite:sunny'
}
}
});
In this example, the icon sunny
from the sprite sheet loaded with the unique identifier my-sprite
will be used as the icon for the layer:
While ideally all of your icons would be in a single sprite sheet to reduce the number of image requests, you can load multiple sprite sheets into MapsGL and use icons from any of them in your styles. Just make sure to use the correct unique identifier when referencing the icon in the icon.image
property.
Adding Remote Images
If you prefer to load individual images for each icon instead of using a sprite sheet, you can use the loadImage
method on the MapController#style
instance to load images from URLs. This method takes a unique identifier for the image and the URL of the image to load. Additional options can be provided to control how the image is loaded, such as the image's pixel density or whether the image is an SDF (signed distance field) image:
// Load an image from a URL
controller.style.loadImage('my-icon', 'https://example.com/icon.png');
controller.style.loadImage('my-icon-2x', 'https://example.com/icon@2x.png', { pixelRatio: 2 });
// Load an SDF image from a URL
controller.style.loadImage('my-sdf-icon', 'https://example.com/icon.png', { sdf: true });
The loadImage
method returns a Promise
that resolves when the image is loaded and ready to use, which you can then use to ensure that the image is ready before using it in your layer paint styles:
controller.addLayer('my-layer', {
type: 'symbol',
source: 'my-source',
paint: {
icon: {
image: 'my-icon'
}
}
});
In this example, the image loaded with the unique identifier my-icon
will be used as the icon for the layer.
Adding Images
You can also add images directly to MapGL's image manager using the addImage
method on the MapController#style
instance. This method takes a unique identifier for the image, the raw image data as a Uint8Array
, and the image's dimensions. You can also provide additional options, such as the image's pixel density or whether the image is an SDF (signed distance field) image:
// Add an image from a Uint8Array
controller.style.addImage('my-icon', {
data: new Uint8Array([...]),
size: { width: 32, height: 32 }
});
You can also add an image drawn from a canvas element by passing in the canvas context's image data:
const size = 36;
const radius = size / 2;
const lineWidth = 2;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
ctx.lineWidth = lineWidth;
ctx.arc(canvas.width / 2, canvas.height / 2, radius - lineWidth, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
controller.style.addImage('my-icon', {
data: ctx.getImageData(0, 0, canvas.width, canvas.height).data,
size: {
width: canvas.width,
height: canvas.height
}
});
The addImage
method does not return a Promise
as the image is added synchronously to the image manager. You can then use the image in your layer paint styles immediately after adding it by referencing the unique identifier in the icon.image
property:
controller.addLayer('my-layer', {
type: 'symbol',
source: 'my-source',
paint: {
icon: {
image: 'my-icon'
}
}
});
Adding Animated Images
In addition to static images, you can also add animated images to MapsGL that are generated at runtime using the Canvas API (opens in a new tab) and drawn on every frame as needed. For these images, you will use the same addImage
method on the MapController#style
instance. But you will also need to provide a renderer
object that implements the rendering interface required for rendering the image and returning its data on each frame:
interface StyledImageRenderer {
data: Uint8Array | Uint8ClampedArray;
size: Size;
initialize?: (data: Uint8Array | Uint8ClampedArray, size: Size) => void;
draw?: (data: Uint8Array | Uint8ClampedArray, size: Size) => Uint8ClampedArray | undefined;
dispose?: () => void;
}
For instance, the following example shows how to add an animated image that changes color over time:
const iconSize = 60;
controller.style.addImage('my-animated-icon', {
// Initialize the image data with an empty Uint8Array based on the icon size
data: new Uint8Array(iconSize * iconSize * 4),
// Define the image size
size: { width: iconSize, height: iconSize },
// Set the pixel ratio to 2 for retina displays, meaning the image will appear at 30x30 pixels on screen.
pixelRatio: 2,
// Provide a renderer object that implements the rendering interface
renderer: {
// Called when the image is first added to MapsGL, so perform any necessary setup here
initialize(data, { width, height }) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
this.context = canvas.getContext('2d');
},
// Called on every frame to draw and return the image data
draw(data, { width, height }) {
const ctx = this.context;
const duration = 1000;
const t = (performance.now() % duration) / duration;
const hue = t * 360;
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
ctx.fill();
return ctx.getImageData(0, 0, width, height).data;
}
}
});
Data-Driven Icons
Similar to many other paint style properties, you can also use data-driven styling to change the appearance of symbols based on individual feature properties.
For example, if you're showing current weather observations for select locations, you can change the icon image based on the observation and weather data at each location. The following example uses the custom weather icon sprite sheet from earlier to change the icon image based on the weatherPrimary
property in the feature's data:
controller.addLayer('obs', {
type: 'symbol',
source: 'obs',
paint: {
icon: {
image: (data) => {
const weather = data.ob.weatherPrimary.toLowerCase();
let icon = 'sunny';
if (/partly|mostly/.test(weather)) {
icon = 'partly-cloudy';
} else if (/cloudy|overcast/.test(weather)) {
icon = 'cloudy';
} else if (/rain|drizzle|mist/.test(weather)) {
icon = 'rain';
} else if (/snow|sleet|ice/.test(weather)) {
icon = 'snow';
} else if (/fog|smoke|haze/.test(weather)) {
icon = 'cloudy';
} else if (/storm|thunder|lightning/.test(weather)) {
icon = 'storms';
}
return `my-sprite:${icon}`;
},
size: { width: 36, height: 36 }
}
}
});
SDF Icons
If you want to use the same icon for each feature but change its fill and/or stroke colors based on feature properties, you can use signed distance field (opens in a new tab) (SDF) icons. SDF icons are single-color icons that can be recolored at runtime using the fill.color
and stroke.color
properties in the layer's paint styles. Another benefit of SDF icons is that they can be scaled to any size without losing quality or sharpness.
While generating SDF icons is beyond the scope of this documentation, you can find a number of tools and libraries (opens in a new tab) available to help you create them. An SDF encodes the distance from each pixel to the nearest edge of the icon shape such that pixels that are pure white(1) are right next to or within the icon shape, and pixels that are pure black (0) are farthest away from the icon shape with a smooth gradient from 1 to 0 in between:
Once you have an SDF icon, you can load it into MapsGL using the loadImage
method and then reference it in the layer's paint styles along with the fill color based on individual feature properties:
controller.style.loadImage('lightning-icon', 'https://example.com/lightning-icon.png', { sdf: true });
// Use the custom SDF icon for the built-in lightning-strikes layer
controller.addWeatherLayer('lightning-strikes', {
type: 'symbol',
paint: {
icon: {
image: 'lightning-icon',
size: { width: 28, height: 28 }
},
fill: {
color: (data) => {
const age = data.age || 0;
if (age >= 600)
return '#C4150D';
else if (age >= 300)
return '#F97002';
else if (age >= 120)
return '#F9E203';
return '#EEEEEE';
}
},
stroke: {
color: '#000',
thickness: 1
}
}
});