MapboxGL part 2 - NieneB/webmapping_for_developers GitHub Wiki

Manipulating the style with JavaScript

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 style

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

▶️ Have a look at the Topo map style from Webmapper topography.json

▶️ Also look at the Cartiqo Documentation

img

I developed several styles for the Cartiqo tiles. Have a look at some more JSON style files I developed:

Layers?!?!

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.

Get feature information in pop-up

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.

▶️ Add the following code to your JavaScript to create a map click event.

    // 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.

▶️ Look at the following code:

// 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.

▶️ Try out the code and see if you can discover how the tile set is build up. Also look at the Cartiqo Documentation

Change a style layer

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

▶️ Change the color of the water style layer:

map.on('load', function(){
    map.setPaintProperty('water', 'fill-color', '#ee00ff');
});

▶️ Use the style to change more layers and colors of the map.

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');
});

Removing style layers

We can also remove style layers. (we cannot remove data layers in the vector tiles, because this is a given thing)

▶️ Use the 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.

▶️ Set the visibility to none to hide the place labels in the map:

map.setLayoutProperty("place-labels", "visibility", "none")

Adding a style layer

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.

▶️ Create a layer and add it with 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?)

▶️ Put it behind the highway-case, and change the style so it looks like a shadow:

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")
})

Writing functions

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.

Font size increase/decrease

▶️ Add 2 buttons to the HTML with a 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>

▶️ Create a function which takes the style and finds all the text containing layers.

// 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!

Add extra source

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

▶️ Make your own GeoJSON here

Interaction on GeoJSON source

Next to our basic GeoJSON layer we need a Hover layer.

▶️ Create an extra 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'
    }
  });
});

▶️ Add a mousemove action:

 //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", ""]);
});

Data driven styling > Using Expressions

▶️ Use the style specification to see if you can use an 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.

⚠️ **GitHub.com Fallback** ⚠️