mapbox-gl-js
mapbox-gl-js copied to clipboard
Support icon-color for non-SDF icons
We have a use case that requires the ability to use icons for data-driven shapes as well as data-driven coloring of those icons. Currently we can get half of this functionality from the circle
layer type and half from the symbol
layer type. We have a hacked solution that merges the basic features of both on a fork of mapbox-gl-js (along with a style-spec and shader fork)...
https://github.com/ivelander/mapbox-gl-js
It would be great to be able to get off this fork, something we could do if support was added for icon-color
for non-SDF icons.
Here is an example of the result I am after, this uses the fork linked in the first comment to use a sprite sheet texture and a property based color.

This would be great to support on fill-pattern
and line-pattern
too 😄
Just in case this helps anyone -- I solved this issue on my map by using a custom icon font (I used http://fontastic.me but FontAwesome or anything else will work just the same). Just upload the font to mapbox studio and use the text-font, text-color, text-size, etc styling options for the geoJson layer and just use no icon. You can do some pretty awesome stuff with that and it has the added benefit of making it much easier to set up an icon baseline size and scale it up or down (vs my previously uploaded SVGs to mapbox studio which all ended up slightly different sizes).
@rhagigi any chance you can post a full example / screenshot?
Sure thing, I'll put something together when I get home.
The way I'm achieving it is fairly simple. As I mentioned before, simply create a font using fontastic or another font generation service/build-tool, or use an existing icon font like fontawesome or material-icons. Then:
-
Upload that font on the Mapbox Studio Font Page. a) I think I had to also use the font on some layer in the map to get it to come down with the map and be usable. Could be a fake layer or just a layer you're not even really using in your style, like maybe "airport labels" or something obscure. Make a note of what the font is called there, you'll need it later see example screenshot
-
Add a GeoJSON source to the map as usual
map.addSource('MySourceId', {
type: 'geojson',
data: geoJson
});
- Add a
symbol
layer to the map to show that source, using our font and a bunch of layout/styling tricks to get it working to your tastes:
map.addLayer({
id: 'MyLayerId',
type: 'symbol',
source: 'MySourceId',
layout: {
'text-line-height': 1, // this is to avoid any padding around the "icon"
'text-padding': 0,
'text-anchor': 'bottom', // change if needed, "bottom" is good for marker style icons like in my screenshot,
'text-allow-overlap': true, // assuming you want this, you probably do
'text-field': iconString, // IMPORTANT SEE BELOW: -- this should be the unicode character you're trying to render as a string -- NOT the character code but the actual character,
'icon-optional': true, // since we're not using an icon, only text.
'text-font': ['FontAwesome Regular'], // see step 1 -- whatever the icon font name,
'text-size': 18 // or whatever you want -- dont know if this can be data driven...
},
paint: {
'text-translate-anchor': 'viewport', // up to you to change this -- see the docs
'text-color': '#00FF00' // whatever you want -- can even be data driven using a `{featureProperty}`,
}
});
For more layout/paint properties you can use (like pitch/rotation styling, etc) and more info, visit the Mapbox Style spec page for symbol layers
- In order to set the text field, you need to actually have a string value of the icon you want. If you already know the character code for the one you need, you can just hardcode it and use
String.fromCharCode
to get a string value from that character code from the font css. For example, fa-calendar is"\f073"
.
But, that can be hard when you don't know what the character will be in the font and you want to use the "classname" version for this, like fa-calendar
or fa-plane
or whatever. This makes you more resilient in case you upgrade font versions, as the underlying character-code mapping may change. Make sure you load the font in your CSS as well for this technique -- I use fontawesome on my site anyway so it wasn't extraneous.
This is kinda-font-awesome specific, as they load the character using CSS into the :before
content
based on the classname (fa-calendar example), but you can use a similar technique for most icon fonts. I wrapped my function in lodash memoize as once you call this once for a classname, you should never need to call it again.
const getFontAwesomeStringFromClassname = _memoize((className) => {
const element = document.createElement('i');
element.className = 'fa ' + className; // font-awesome specific where `className`==="fa-calendar"
element.style.display = 'none'; // or not
document.body.appendChild(element);
const contentValue = window.getComputedStyle(
element, ':before'
).getPropertyValue('content');
document.body.removeChild(element);
return contentValue;
});
You can then pass that returned contentValue into text-field
or -- if you want, use it in your GeoJSON source to use different icons for different features, and use the {someFeatureProperty}
syntax to grab the textField from each feature property. I suggest a similar approach for the text-color
property if you want to drive it based on data.
Unfortunately, I'm not sure if mapbox-gl supports data-driven styling yet for font-size (they might now), so I just used multiple layers with filters on them to do dynamic sizing (e.g.: one layer size 40 and one layer size 20, with filters for the feature type).
Hopefully that wasn't too much craziness and I didn't skip anything important. Let me know if you have any questions about all that.
That's great Royi, thanks!
We are using a similar strategy as well. I'd be curious if anyone (perhaps the mapbox folks) have any input on potential performance or stability concerns with this strategy. I know there is a lot of logic in the text placement logic to manage collisions, positioning in relation to the icon, etc. Our apps can result in a lot of data on the map, so performance quickly becomes a concern with everything we are doing (part of the appeal of mapbox-gl in the first place).
We've solved this a different way!
We've used circles and icons as separate layers ... see this:
icon_layer = {
"id": "images",
"type": "symbol",
"source": "markers",
"layout": {
"icon-image": "{icon}-15",
"icon-allow-overlap": True,
"icon-ignore-placement": True,
"icon-size": {
"base": 1,
"type": "exponential",
"stops": [
[10, 0],
[12, 0.8],
[15, 0.8],
[18, 1]
]
},
}
}
circle_layer = {
"id": "markers",
"type": "circle",
"source": "markers",
"paint": {
'circle-radius': {
'property': 'task-priority',
'type': 'categorical',
'stops': [[1, 5], [5, 5], [10, 10]],
},
'circle-color': {
'property': 'task-status',
'type': 'categorical',
'stops': list(qs.model.STATUS_CHOICES_COLOURS.items()),
},
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
'circle-stroke-opacity': 1,
}
}
layers = [circle_layer, icon_layer]
This is what it looks like (if you want the icons inside the circles, change base size to 0.7):
@rhagigi Thanks for the detailed description. I'm almost there.
I'm finding that even though I have added FontAwesome (or Material Icons) as a font to my Mapbox style, and even though some version of that font gets downloaded by the mapbox component, no markers show up.
If i use a font that I know exists like Arial Unicode MS Bold
then I see their (ugly ;) letters.
I believe the font isn't being properly downloaded, based on the network traffic:

56 bytes seems a bit short. The file is a PBF file that contains:
"
FontAwesome Regular65280-65535
Any suggestion how to debug this?
One hint might be that the font name isn't actually being displayed:

(it'd be lovely if mapbox just published these two very common and openly licensed fonts)
Hmm, reading on the API (https://www.mapbox.com/api-documentation/#fonts) it seems that API call is just getting a single glyph, which should be fine. Maybe it's just my character lookup isn't correct.
Figured it out. The mapping from icon to a string was where I was failing.
For the next people who run into this issue, here are the incantations to map from a font-awesome or Material Design Icons to a string that will fit in with @rhagigi's method above.
For Font-Awesome, find the icon like, e.g. http://fontawesome.io/icon/camera/, you'll notice it says it is at Unicode location f030
.

The string you want is therefore: String.fromCharCode("0xf030")
.
For Material Icons, you'll notice in the instructions for Web Font under "IE usage" that the entity is

.

That corresponds to using the string String.fromCharCode("0xe8fc")
.
It's definitely a manual mapping, but it helped me out.
I was running into an issue trying to use Glyphicons pro icon packs inside of an Ionic project using Mapbox GL.
In Ionic, typescript forces String.fromCharCode()
to be accept only a number, so you run into issues when you need to map to the higher end Unicode characters. For the character I wanted (shown as UTF+E592 on the Glyphicons website) I was trying `String.FromCharCode(592)' and just getting blank characters. And I was unable to use e592 or other variations due to the above typing limitations.
e.g 0xe592 is not a number so you get compilation errors. See: MDN FromCharCode - Getting it to work with higher values
The solution was to actually use String.fromCodePoint()
which accepts an octet: String.fromCodePoint(0xe592)
The biggest issue I have encountered using non-SDF icons is that icon-halo-XXX
options do not work. non-SDF with valid a-channel seem to work as expected with icon-color
@davidascher @rhagigi Trying your methods to use Google's Material Icons [I've also tried with Font Awesome] and I've not been able to get the icons to show up. I've tested the Material Icons on other parts of the page (ie: ) without problems and used your exact coding. The layer also works with the Maki icons.
The icon is also showing up in console after using String.formCharCode().
Here's the code:
@rhagigi Does this method only work using Mapbox Studio-hosted fonts? Or can I use the Google Material Icons CDN?
For reference, here's the manual way to convert font for use as symbol text layer:
-
Install genfontgl (https://github.com/sabas/genfontgl) via npm
-
Convert font to pbf directory by following genfont instructions. Directory will be a folder of .pbf files (see below):
-
Host all fonts you want to show up on your style (including the icon fonts [Material Icons or FontAwesome]) in a folder. Folder should look like this:
-
Upload to a server (Github, S3, etc)
-
On style.json file, change glyphs address to ("glyphs": "your_url_here/{fontstack}/{range}.pbf")
-
Now you can follow @rhagigi 's instructions above.
Hope this helps!
@Kilatsat I can't reproduce your comment that icon-color
does work with images with an alpha channel. I tried with png files that are partially transparent. Am I doing something wrong?
@everhardt I should have been more clear in my previous response: it appears to respect a=0. I did not test semitransparent images.
@Kilatsat I'm sorry, I'm still not clear on what you are saying. Do you say that it's possible to change the color of certain non-SDF images or are you saying that it is possible to make an image invisible (completely transparent, alpha = 0) by changing the icon-color
paint property to some value.
I fail to achieve either, but I'm interested in the former..
@everhardt I can help you modify an existing example to illustrate the point I was originally making:
- Start from the code contained in https://www.mapbox.com/mapbox-gl-js/example/add-image/
- Modify line 29 to read
map.addImage('cat', image, { sdf: true });
- In the
map.addLayer
add a new section that reads"paint": {"icon-color": "orange"}
This modification causes the cat to appear orange
instead of the default black
even though the cat image is non-SDF.
@Kilatsat Thanks a lot, that is exactly what I was looking for!
I have thoroughly investigated the issue. The problem is in the use of map.loadImage()
function. If we reference a file by its path i-e map.loadImage('../../cat.png')
then even if you make { sdf: true }
, you will not able to change the color and other related features. If you want to change color you have to convert the image into its base64 equivalent. const catImg = require('../../cat.png');
and then use the map.loadImage()
function to load it. Now if you do { sdf: true }
you can successfully change the color or related features.
@rhagigi I opened a pull request, so hopefully this will be easier in the future: https://github.com/openmaptiles/fonts/pull/9 @stdmn You can download all pdb files from here (including my addition of Font Awesome 5 Free solid icons): https://github.com/danger89/fonts/tree/gh-pages
To shown the icon you need to use the hex value as a string. Eg for a nice marker icon of Font Awesome 5 solid, try:
"layout": {
'text-line-height': 1, // this is to avoid any padding around the "icon"
'text-padding': 0,
'text-anchor': 'center', // center, so when rotating the map, the "icon" stay on the same location
'text-offset': [0, -0.3], // give it a little offset on y, so when zooming it stay on the right place
'text-allow-overlap': true,
'text-ignore-placement': true,
'text-field': String.fromCharCode("0xF3C5"),
'icon-optional': true, // since we're not using an icon, only text.
'text-font': ['Font Awesome 5 Free Solid'],
'text-size': 35
},
"paint": {
'text-translate-anchor': 'viewport',
'text-color': ['get', 'color'] // get color from the properties geojson file !
}
geojson data file should have atleast "color" property, eg:
{ ..., "properties":{"color": "#86EA66"}, "geometry": .....
North up 2D:
Rotated & tilted:
Rotated & tilted 180-degrees:
Zoomed-out:
Piggy-backing on this issue since I recently ran into a need for coloring icons based on data. Currently working around it by using multiple SVGs, though I like the font-awesome workaround suggested above.
Coloring icons would be an excellent feature to add for the cost of a single multiply in our shader and 24 bits of color data.
@everhardt I can help you modify an existing example to illustrate the point I was originally making:
- Start from the code contained in https://www.mapbox.com/mapbox-gl-js/example/add-image/
- Modify line 29 to read
map.addImage('cat', image, { sdf: true });
- In the
map.addLayer
add a new section that reads"paint": {"icon-color": "orange"}
This modification causes the cat to appear
orange
instead of the defaultblack
even though the cat image is non-SDF.
Why this possiblity is not documented? Me help this information a lot!
For anyone reading this I have came across the following solution (haven't tried it yet), which is different than using font awsome characters (which I think is problematic if you read the style from a json file without replacing anything like I need to):
https://www.npmjs.com/package/@elastic/spritezero
https://www.npmjs.com/package/@elastic/spritezero-cli
This fork can create an sdf image from SVG files.
These generated files can be used as sprites for mapbox-gl to receive the required behavior.
Here's an example (I didn't write it just came across it):
http://www.npeihl.com/maki-sdf-sprites/
I have looked at the code and it basically loads the image to Image
DOM element and adds a single sdf image using addImage(... { sdf: true})
.
I hope it can be used as a sprite link instead but I haven't tried it yet.
@nickpeihl can you confirm?
I hope it can be used as a sprite link instead but I haven't tried it yet. @nickpeihl can you confirm?
@HarelM Are you asking if it's possible to set the URL to the spritesheet in a style definition? I think that should work, because the spritesheet.json file (example) includes { sdf: true }
for each sprite.
@nickpeihl thanks for the info!
Here's an example of a colored icon using your example sdf sprite.
https://stackblitz.com/edit/mapbox-simple-map-sdf-sprite
The only caveat I found is that spritezero-cli can't be installed on windows. It can be installed using ubuntu linux container with docker, but it's a hassle...
Here is a simple docker image to convert a folder of ./svg/*.svg
files into a folder of ./dist/sprite*
, with {sdf: true}
set for each sprite :
docker run -v $(pwd)/dist:/dist -v $(pwd)/svg:/svg fredmoser/svg_to_sprite:latest
Repo : svgToSprite
Hopefully that wasn't too much craziness and I didn't skip anything important. Let me know if you have any questions about all that.
This is a very helpful work around, thanks @rhagigi!
- Upload that font on the Mapbox Studio Font Page. a) I think I had to also use the font on some layer in the map to get it to come down with the map and be usable. Could be a fake layer or just a layer you're not even really using in your style, like maybe "airport labels" or something obscure
For shame mapbox :/