MapboxGL part 2 - NieneB/webmapping_for_developers GitHub Wiki
We can change the vector tile map on the client side with JavaScript. From mouse interaction to changing colors to dynamically updating text size etc. But in order to do this we need to understand the style object a bit more.
The map style itself is written as rules which define its visual appearance using the Mapbox GL Style Specification. It specifies:
- What data to draw: Sources
- What order to draw the data in: Layers
- How to style the data when drawing it: Layers: Paint and Layout properties
- Which fonts and icons to use: Glyphs & Fonts
This is the basics of a style:
{
"version": 8,
"name": "Mijn eigen Stijl",
"sprite": "url",
"glyphs": "url/{fontstack}/{range}.pbf",
"sources": {...},
"layers": [...]
}
The style definition is most commonly supplied in a JSON
format. 🔗 Mapbox provides good documentation on the style specifications
I developed several styles for the Cartiqo tiles. Have a look at some more JSON style files I developed:
- https://ta.webmapper.nl/wm/styles/crafty.json
- https://ta.webmapper.nl/wm/styles/data_lines.json
- https://ta.webmapper.nl/wm/styles/1943.json
In Leaflet we call every data set or information source a Layer. We can have raster layers and vector layers. Even data layers, like geojson or image layers are possible. A Layer in Leaflet means adding some visible information from a specific source as a layer to the map.
In MapboxGL we do not see the source data sets as layers! We define Sources and inside these sources we can have source Layers. A source layer is an individual layer of data within a vector source. A vector source can have multiple layers. Having a source and source layers does not mean it is visible on the map! Therefore we need a Style Layer. A style layer is used to add styling rules to a specific subset of data in a map style. Data from different sources can be mixed and matched in the client side by defining the style layer. (In MapboxGL there exists no base layer like in Leaflet)
So when making a MapboxGL map we need a Source (which can be a vector tile source) and a Style defining which data needs to be rendered, when and how. Actually the sources are defined in the map style.
It is very handy to see the attribute information of the feature on a mouse click. As a pop-up or in the console. This can help us to understand the vector tile source a bit more and practices our user interaction possibilities.
// Click event
map.on('click', function (e) {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML('You clicked at: ' + e.lngLat)
.addTo(map);
});
ℹ️ We use the mapboxgl.Popup()
instance. See the documentation https://docs.mapbox.com/mapbox-gl-js/api/#popup
If we want to know what feature we clicked on we need to query
the tile source.
// Click event
map.on('click', function (e) {
let features = map.queryRenderedFeatures(e.point);
var text = '';
if (features.length > 0) {
features.map((f, i) => {
text = text + `<h3>Layer ${i}: ${f.sourceLayer}</h3>`;
for (const key in f.properties) {
if (f.properties.hasOwnProperty(key)) {
text = text + `<b> ${key}: </b>${f.properties[key]}</br>`
}
}
})
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(text)
.addTo(map);
}
});
ℹ️ map.queryRenderedFeatures()
queries the tile source but only query the data layers which are rendered, and so have at least one style layer. It returns an array of GeoJSON Feature objects representing visible features that satisfy the query parameters.
🔗 https://docs.mapbox.com/mapbox-gl-js/api/#map#queryrenderedfeatures
ℹ️ On one click, multiple style layers can be queried.
The above function shows us which data is present. The next things we will do are about the style layers.
With JavaScript we are able to change a style layer's appearance paint
properties and layout
properties. Also we can delete and add layers with addLayer
and removeLayer
map.on('load', function(){
map.setPaintProperty('water', 'fill-color', '#ee00ff');
});
See the style object at https://ta.webmapper.nl/wm/styles/topography.json and see which style layers there are and how you can change them!
For example:
// Change style layer
map.on('load', function () {
map.setPaintProperty('water', 'fill-color', '#40bcd8');
map.setPaintProperty('highway-fill', 'line-color', '#ea4f57');
map.setPaintProperty('highway-case', 'line-color', '#a7363b');
map.setPaintProperty('road-fill-main-motor', 'line-color', '#ffaf3f');
map.setPaintProperty('road-case-main-motor', 'line-color', '#a7363b');
});
We can also remove style layers. (we cannot remove data layers in the vector tiles, because this is a given thing)
getLayer
to check if a layer is present. Then remove it with removeLayer
:
// Remove Style layer or visibility
map.on('load', function () {
if (map.getLayer('place-labels')) { map.removeLayer('place-labels') };
});
If you do not want to remove a layer, and keep it to later display it again, we can also toggle it's visibility. This is a layout
property.
map.setLayoutProperty("place-labels", "visibility", "none")
We can add more layers by using the available source. So the source is wm_visdata
as defined in the style object. We can look at the Cartiqo content to see which data layers we can add. Then define a style.
We can create infinite style layers, accordingly on what data layers are available in the vector data. So one data layer can contain multiple style layers.
addLayer
:
map.on('load', function(){
map.addLayer({
id: "newlayer",
source: "wm_visdata",
source-layer: "roads",
type: "line",
filter: [
"==",
[
"get",
"type"
],
"highway"
],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"paint": {
"line-width": 20,
"line-color": "#317581"
}
})
})
ℹ️ The "id"
is a custom name you give to the layer. You can give it any name you like.
ℹ️ The "source"
is the name of the source provided in the style object. We called it "wm_visdata"
.
ℹ️ The "source-layer"
is the name of the data layer in the vector tiles. This information is fixed and can be found in the Cartiqo Documentation.
ℹ️ "type"
is the rendering type of the layer. It can be one of fill
, line
, symbol
, circle
, fill-extrusion
, raster
or background
. See the mapbox style spec.
ℹ️ "paint"
Default paint properties for this layer.
ℹ️ "layout"
the optional layout properties for this layer.
There are more options you can give to a layer. For example:
- filter
- minzoom
- maxzoom
If we want to put this style layer behind the already existing roads we can add the style layer id name to the addLayer()
function. https://docs.mapbox.com/mapbox-gl-js/api/#map#addlayer
map.addLayer(layer, beforeID?)
map.on('load', function(){
map.addLayer({
id: "newlayer",
source: "wm_visdata",
source-layer: "roads",
type: "line",
filter: [
"==",
[
"get",
"type"
],
"highway"
],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"paint": {
"line-width": 20,
"line-color": "#317581",
"line-blur": 10,
"line-translate": [5,5]
}
}, "highway-case")
})
A fun way to make your style interactive is writing our own functions with the map methods and events. For example creating buttons to increase and decrease the font size of ALL the labels.
You can use this example or try to write your own.
onclick
function:
<button onclick="changeFontSize(1.4);" > <span style="font-size:10px;">A</span><span style="font-size:14px;">A</span><span style="font-size:18px;">A</span></button>
<button onclick="changeFontSize(0.7);"> <span style="font-size:18px;">A</span><span style="font-size:14px;">A</span><span style="font-size:10px;">A</span></button>
// Increase fonts for bad sighted people
const changeFontSize = function (factor) {
let stijl = map.getStyle();
stijl.layers.forEach((layer) => {
if (layer.type === "symbol" && layer.layout["text-size"]) {
let currentsize = layer.layout["text-size"];
if (currentsize.length > 4) {
for (let i = 4; i <= currentsize.length; i += 2) {
currentsize[i] = currentsize[i] * factor;
};
map.setLayoutProperty(layer.id, 'text-size', currentsize);
}
else if (typeof currentsize === "number") {
map.setLayoutProperty(layer.id, 'text-size', currentsize * factor);
}
else { console.log("different style for text size") }
}
})
};
This code works on (almost?) all styles!
We can add extra Sources to the style object. For example a GeoJSON file, a different vector tiles source, a raster map source etc.
Let's use the GeoJSON from the Leaflet example.
let data = {};
// adding WFS geojson
let url = "https://geodata.nationaalgeoregister.nl/wijkenbuurten2018/wfs?service=WFS&version=2.0.0&"
+ "request=GetFeature&"
+ "outputFormat=application/json&"
+ "srsName=EPSG:4326&"
+ "geometryLength=5&"
+ "TypeName=wijkenbuurten2018:cbs_buurten_2018&"
+ "propertyName=buurtnaam,aantal_inwoners,geom&"
+ "cql_filter=(water='NEE' AND gemeentenaam='Amsterdam')";
let xhr = new XMLHttpRequest()
xhr.open('GET', encodeURI(url))
xhr.onload = function () {
if (xhr.status === 200) {
data = JSON.parse(xhr.responseText)
} else {
alert('Request failed. Returned status of ' + xhr.status)
}
}
xhr.send();
// On Load add GeoJSON SOURCE and LAYER
map.on('load', function (e) {
// ADD GEOJSON SOURCE
map.addSource('gemeenteGeoJSON', {
'type': 'geojson',
'data': data
});
// ADD layer
map.addLayer({
'id': 'geojson-polygons',
'type': 'fill',
'source': 'gemeenteGeoJSON',
'layout': {},
'paint': {
'fill-color': '#000fff'
}
});
});
ℹ️ with map.addSource
we add an extra source to the map.
ℹ️ map.addLayer
adds an extra layer to the map, on top off the map.
🔗 Read about GeoJSON format here
Next to our basic GeoJSON layer we need a Hover layer.
We make the layer with an empty filter. So it does not show up on the map on initialization. With the mousemove
event we can change the filter so the layer is rendered.
map.on('load', function () {
// ADD an extra layer
map.addLayer({
'id': 'geojson-polygons-hover',
'type': 'fill',
'source': 'gemeenteGeoJSON',
"filter": ["==", "name", ""],
'layout': {},
'paint': {
'fill-color': '#ff00ee'
}
});
});
//Adding hover effect
map.on("mousemove", "geojson-polygons", function (e) {
map.setFilter("geojson-polygons-hover", ["==", "buurtnaam", e.features[0].properties.buurtnaam]);
});
// Reset the state-fills-hover layer's filter when the mouse leaves the layer.
map.on("mouseleave", "geojson-polygons", function () {
map.setFilter("geojson-polygons-hover", ["==", "buurtnaam", ""]);
});
expression
to color the Neighbourhoods on the amount of inhabitants.
a tip:
["interpolate", ["linear"] , [ "get", "aantal_inwoners"],
0, #FFEDA0,
6000, #800026]
Let's continue with MapboxGL part 3 to build your own style json file from scratch.