Boundless has recently released ol-mapbox-style, a utility to use Mapbox's Style format for styling vector and vector tile layers in OpenLayers. In this blog post, I'll be showing how to use this new utility. But let's get started with an overview of how styling works in Boundless Suite, and how the Mapbox Style format can make it easier.
Boundless Suite's GeoServer ships with the usa:states demo layer:
While this map is neither beautiful nor useful, it helps understand two important basic concepts of web cartography:
- Feature attribute dependent styling - State polygons are colored in three classes, depending on their absolute number of inhabitants.
- Scale dependent styling - When zoomed out, no labels are shown. As the user zooms in, state abbreviations show up. As the user zooms in further, state names will be used to label state polygons.
GeoServer uses SLD, an XML format, for styling. Boundless Suite's GeoServer allows users to use YSLD instead, which is a more compact YAML representation of the same format. In SLD, feature attribute dependent styling is achieved with rules that have a filter and symbolizers:
<Rule>
<Name>Population < 2M</Name>
<ogc:Filter>
<ogc:PropertyIsLessThan>
<ogc:PropertyName>PERSONS</ogc:PropertyName>
<ogc:Literal>2000000</ogc:Literal>
</ogc:PropertyIsLessThan>
</ogc:Filter>
<PolygonSymbolizer>
<Fill>
<CssParameter name="fill">#A6CEE3</CssParameter>
<CssParameter name="fill-opacity">0.7</CssParameter>
</Fill>
</PolygonSymbolizer>
</Rule>
Scale dependent styling is done with rules that have a MinScaleDenominator
or MaxScaleDenominator
element:
<Rule>
<Name>State Abbreviations</Name>
<MinScaleDenominator>17500000</MinScaleDenominator>
<MaxScaleDenominator>35000000</MaxScaleDenominator>
<TextSymbolizer>
<Label><ogc:PropertyName>STATE_ABBR</ogc:PropertyName></Label>
<Font>
<CssParameter name="font-family">Arial</CssParameter>
<CssParameter name="font-size">12</CssParameter>
</Font>
</TextSymbolizer>
</Rule>
Styling vector and vector tile layers in OpenLayers 3 is very powerful, but it requires basic JavaScript skills because it is done with functions. The disadvantage of this concept is that styles can not be serialized for export. A style function is called with the feature being styled and the rendering resolution as arguments. With these, it is easy to apply both attribute based and resolution dependent styling through simple JavaScript if
statements. The style function returns an ol.Style
, or an array of ol.Style
:
var style = new ol.style.Style({
fill: new ol.style.Fill(/* ... */),
text: new ol.style.Text(/* ... */)
});
function(feature, resolution) {
var population = feature.get('PERSONS');
if (population <= 2000000) {
style.getFill().setColor('rgba(166,206,227,0.7)');
}
if (resolution < 9260 && resolution >= 4630) {
style.getText().setText(feature.get('STATE_ABBR'));
}
// ...
return style;
}
Compared to SLD and CSS based style formats, Mapbox Style has some significant advantages:
- JSON - easy to read and modify by applications like graphical style editors
- Built-in concept of sources and layers - can describe the whole map, not just a single layer
- Functions to control the appearance across a range of zoom levels / resolutions without specifying distinct styles for resolution ranges
For attribute based styling, there is an easy to use filter syntax:
{
"id": "population_lt_2m",
"type": "fill",
"source": "states",
"filter": ["<=", "PERSONS", 2000000],
"paint": {
"fill-color": "#A6CEE3",
"fill-opacity": 0.7
}
}
Resolution based styling is done through zoom levels:
{
"id": "state_abbreviations",
"type": "fill",
"source": "states",
"minzoom": 4,
"maxzoom": 4,
"layout": {
"text-field": "{STATE_ABBR}",
"text-size": 12,
"text-font": ["Arial Normal"]
}
}
You may also want to take a look at the complete Mapbox Style object for the usa:states layer.
With this utility, there is no need any more to write JavaScript style functions for OpenLayers. A minimal example to display and style the usa:states
vector layer in OpenLayers would look like this:
fetch('states.json').then(function(response) {
response.json().then(function(glStyle) {
map.addLayer(new ol.layer.Vector({
style: olms.getStyleFunction(glStyle, 'states'),
source: new ol.source.Vector({
format: new ol.format.GeoJSON(),
url: '/geoserver/wfs/?service=wfs&version=1.1.0&request=getfeature' +
'&typename=usa:states&outputformat=application/json'
})
}));
});
});
This loads the Mapbox Style JSON from states.json
and uses the getStyleFunction()
function of the ol-mapbox-style utility to read the styles for the 'states'
source and convert them into a style function. You can also view the live demo.
Note that the data here is loaded in a single big chunk. It works for this layer, because state polygons are few and the state boundaries are generalized so the number of vertices is quite small. With larger data sets, we will want to use vector tiles instead.
The ol-mapbox-style utility was designed to run directly in the browser. It takes a Mapbos Style object, parses it, and generates an efficient style function for OpenLayers.
In simple cases, like the above, the synchronous getStyleFunction()
function can be used. When additional resources like sprites or fonts need to be loaded, the asynchronous applyStyle()
function provides an easier interface. The above snippet would then look like this:
var layer = new ol.layer.Vector({
style: olms.getStyleFunction(glStyle, 'states'),
source: new ol.source.Vector({
format: new ol.format.GeoJSON(),
url: '/geoserver/wfs/?service=wfs&version=1.1.0&request=getfeature' +
'&typename=usa:states&outputformat=application/json'
})
});
fetch('states.json').then(function(response) {
response.json().then(function(glStyle) {
olms.applyStyle(layer, glStyle, 'states').then(function() {
map.addLayer(layer);
});
});
});
Sprites are automatically loaded by the ol-mapbox-style utility from the url specified as sprite
in the Mapbox Style object. For fonts (or glyphs, as they are called in Mapbox Style), a manual step is required: All fonts used by the Mapbox Style need to be added to the web page as web fonts, e.g.
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Open+Sans" />
This is necessary because OpenLayers currently renders texts using standard browser techniques, and does not build text from glyphs like e.g. Mapbox-GL-JS.
With vector tiles, data is loaded in small chunks that can be cached. Furthermore, tiles are optimized for the zoom level they are loaded for. This technique allows for loading vector data for rendering with minimal bandwidth requirements, especially when compared to raster tiles for HiDPI/Retina devices.
GeoServer can serve vector tiles using the built-in GeoWebCache, with the Vector Tiles extension. On the OpenLayers side, styling works the same as with the example above. And vector tiles is actually what the Mapbox Style format was made for.
If you have a Mapbox access token, you can also try the official ol-mapbox-style example: https://rawgit.com/boundlessgeo/ol-mapbox-style/v0.0.14/example/index.html. It shows how to render Mapbox's "bright" layer with its associated style.
Some Mapbox Style objects reference multiple sources, e.g. the Mapbox "light" style:
"sources": {
"composite": {
"url": "mapbox://mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v7",
"type": "vector"
}
},
OpenLayers currently does not support such comma separated urls directly, but features from multiple sources can be concatenated on a tile level by using a custom tileLoadFunction
on the ol.source.VectorTile
instance:
var layer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
attributions: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> ' +
'© <a href="http://www.openstreetmap.org/copyright">' +
'OpenStreetMap contributors</a>',
format: new ol.format.MVT(),
tileGrid: tilegrid,
tilePixelRatio: 8,
tileLoadFunction: function(tile, url) {
tile.setLoader(function() {
Promise.all([
fetch(url.replace('{source}', 'mapbox-terrain-v2')),
fetch(url.replace('{source}', 'mapbox-streets-v7'))
])
.then(function(responses) {
return Promise.all([
responses[0].arrayBuffer(),
responses[1].arrayBuffer()
]);
})
.then(function(results) {
var format = tile.getFormat();
var features = format.readFeatures(results[0])
.concat(format.readFeatures(results[1]));
tile.setFeatures(features);
tile.setProjection(format.readProjection(results[0]));
});
});
},
url: 'https://{a-d}.tiles.mapbox.com/v4/mapbox.{source}/' +
'{z}/{x}/{y}.vector.pbf?access_token=' + key
})
});
This also brings me to a nice conclusion for this blog post. To show how advanced vector styling is already in OpenLayers. This is how the Austrian Alps look with the Mapbox "light" layer, when rendered with OpenLayers:
But I should also mention what's still missing in OpenLayers vector rendering: advanced label placement (or rather, de-cluttering of labels), and labeling of curves like roads. Both is planned, so stay tuned.