Search code examples
javascripthtmlcssmapbox-gl-jsmapbox-gl

Mapbox GL - highlight features and queryRenderedFeatures() while allowing basemap style change


I need to be able to highlight features in a bounding box using Mapbox GL as shown in this example. My map also needs to have the ability to change the style layer, e.g. changing the base style from mapbox://styles/mapbox/light-v10 to mapbox://styles/mapbox/satellite-v9. This has been challenging because Mapbox GL does not natively support the concept of "basemaps" like, e.g. leaflet does.

My solution, after much searching (I believe I eventually followed a workaround posted in a github issue) involved:

  1. Using map.on('style.load') instead of map.on('load') as shown in the example (I believe map.on('style.load') is not part of their public API, but I could be wrong) and,

  2. Iterating through an array of sources/layers (see the vectorTileLayers variable in the code below) to add vector tiles layers to the map every time a new style is loaded.

This works brilliantly for me in some ways - I'm able to add an arbitrary number of vector tile sources to the array, and they are all added back to the map if the base style is changed. However, I am unable to add a query feature following the example provided by Mapbox if the sources/layers are added to the map in an array and iterated through as shown below. The bounding feature works, but when it is drawn, and released, I see the error show by running the snippet below.

This is a simplified version my actual problem that will allow others to run and manipulate the code. In reality, I'm adding my own vector tile layers (stored in the vectorTileLayers array, which works without issue. When I try and add another layer with the same source and different styling, as shown in the example, I'm unable to actually query the desired layer. Any help would be greatly appreciated. Just FYI - the toggleable layers buttons are not showing up in this snippet, but it's not critical to solve the problem.

mapboxgl.accessToken = 'pk.eyJ1IjoiamFtZXljc21pdGgiLCJhIjoiY2p4NTRzdTczMDA1dzRhbXBzdmFpZXV6eCJ9.-k7Um-xmYy4xhNDN6kDvpg';
    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/light-v10',
      center: [-98, 38.88],
      minZoom: 2,
      zoom: 3
    });

    var vectorTileLayers = [{
        source: {
          type: 'vector',
          url: 'mapbox://mapbox.82pkq93d'
        },
        layer: {
          id: 'counties',
          type: 'fill',
          source: 'counties',
          'source-layer': 'original',
          paint: {
            'fill-outline-color': 'rgba(0,0,0,0.1)',
            'fill-color': 'rgba(0,0,0,0.1)'
          },
        }
      },
      {
        source: {
          type: 'vector',
          url: 'mapbox://mapbox.82pkq93d'
        },
        layer: {
          id: 'counties-highlighted',
          type: 'fill',
          source: 'counties',
          'source-layer': 'original',
          paint: {
            'fill-outline-color': '#484896',
            'fill-color': '#6e599f',
            'fill-opacity': 0.75
          },
          filter: ['in', 'FIPS', '']
        }
      }
    ]

    map.on('style.load', function() {
      for (var i = 0; i < vectorTileLayers.length; i++) {
        var tileLayer = vectorTileLayers[i];
        map.addSource(tileLayer.layer.source, tileLayer.source);
        map.addLayer(tileLayer.layer);
      }

      var layerList = document.getElementById('basemapmenu');
      var inputs = layerList.getElementsByTagName('input');

      function switchLayer(layer) {
        var layerId = layer.target.id;
        map.setStyle('mapbox://styles/mapbox/' + layerId);
      }
      for (var i = 0; i < inputs.length; i++) {
        inputs[i].onclick = switchLayer;
      }

    });

    // Disable default box zooming.
    map.boxZoom.disable();

    // Create a popup, but don't add it to the map yet.
    var popup = new mapboxgl.Popup({
      closeButton: false
    });

    var canvas = map.getCanvasContainer();

    var start;
    var current;
    var box;

    canvas.addEventListener('mousedown', mouseDown, true);
    // Return the xy coordinates of the mouse position
    function mousePos(e) {
      var rect = canvas.getBoundingClientRect();
      return new mapboxgl.Point(
        e.clientX - rect.left - canvas.clientLeft,
        e.clientY - rect.top - canvas.clientTop
      );
    }

    function mouseDown(e) {
      // Continue the rest of the function if the shiftkey is pressed.
      if (!(e.shiftKey && e.button === 0)) return;

      // Disable default drag zooming when the shift key is held down.
      map.dragPan.disable();

      // Call functions for the following events
      document.addEventListener('mousemove', onMouseMove);
      document.addEventListener('mouseup', onMouseUp);
      document.addEventListener('keydown', onKeyDown);

      // Capture the first xy coordinates
      start = mousePos(e);
    }

    function onMouseMove(e) {
      // Capture the ongoing xy coordinates
      current = mousePos(e);

      // Append the box element if it doesnt exist
      if (!box) {
        box = document.createElement('div');
        box.classList.add('boxdraw');
        canvas.appendChild(box);
      }

      var minX = Math.min(start.x, current.x),
        maxX = Math.max(start.x, current.x),
        minY = Math.min(start.y, current.y),
        maxY = Math.max(start.y, current.y);

      // Adjust width and xy position of the box element ongoing
      var pos = 'translate(' + minX + 'px,' + minY + 'px)';
      box.style.transform = pos;
      box.style.WebkitTransform = pos;
      box.style.width = maxX - minX + 'px';
      box.style.height = maxY - minY + 'px';
    }

    function onMouseUp(e) {
      // Capture xy coordinates
      finish([start, mousePos(e)]);
    }

    function onKeyDown(e) {
      // If the ESC key is pressed
      if (e.keyCode === 27) finish();
    }

    function finish(bbox) {
      // Remove these events now that finish has been called.
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('keydown', onKeyDown);
      document.removeEventListener('mouseup', onMouseUp);

      if (box) {
        box.parentNode.removeChild(box);
        box = null;
      }

      // If bbox exists. use this value as the argument for `queryRenderedFeatures`
      if (bbox) {
        var features = map.queryRenderedFeatures(bbox, {
          layers: ['counties']
        });

        if (features.length >= 1000) {
          return window.alert('Select a smaller number of features');
        }

        // Run through the selected features and set a filter
        // to match features with unique FIPS codes to activate
        // the `counties-highlighted` layer.
        var filter = features.reduce(
          function(memo, feature) {
            memo.push(feature.properties.FIPS);
            return memo;
          },
          ['in', 'FIPS']
        );

        map.setFilter('counties-highlighted', filter);
      }

      map.dragPan.enable();
    }

    map.on('mousemove', function(e) {
      var features = map.queryRenderedFeatures(e.point, {
        layers: ['counties-highlighted']
      });
      // Change the cursor style as a UI indicator.
      map.getCanvas().style.cursor = features.length ? 'pointer' : '';

      if (!features.length) {
        popup.remove();
        return;
      }

      var feature = features[0];

      popup
        .setLngLat(e.lngLat)
        .setText(feature.properties.COUNTY)
        .addTo(map);
    });
body {
  margin: 0;
  padding: 0;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

#basemapmenu {
    position: absolute;
    display: inline-block;
    background-color: transparent;
    bottom: 0;
    left: 0;
    margin-left: 10px;
    margin-bottom: 40px;
}

.boxdraw {
  background: rgba(56, 135, 190, 0.1);
  border: 2px solid #3887be;
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
}
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>

<div id="map"></div>

<div id='basemapmenu'>
    <input id='light-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Light' checked='checked'>
    <input id='dark-v10' class='btn btn-outline-primary' type='button' name='rtoggle' value='Dark'>
    <input id='satellite-v9' class='btn btn-outline-primary' type='button' name='rtoggle' value='Satellite'>
  </div>


Solution

  • I ran you code locally and found the problem!

    You are trying to add same source twice and mapbox was giving you error for that, if you check your Console:

    Problem: There can be only one source with one ID on Map

    Error: There is already a source with this ID
    

    After first loop execute successfully. On second iteration, it fails to add same source twice, code breaks here and doesn't continue further. Hence doesn't add second layer(therefore no functionality works thereafter!)

    Fix:(Added a small check to not add same source if already added)

    map.on('style.load', function () {
                for (var i = 0; i < vectorTileLayers.length; i++) {
                    var tileLayer = vectorTileLayers[i];
                    if (!map.getSource(tileLayer.layer.source,)) //FIX
                        map.addSource(tileLayer.layer.source, tileLayer.source);
                    map.addLayer(tileLayer.layer);
                }
    
                var layerList = document.getElementById('basemapmenu');
                var inputs = layerList.getElementsByTagName('input');
    
                function switchLayer(layer) {
                    var layerId = layer.target.id;
                    map.setStyle('mapbox://styles/mapbox/' + layerId);
                }
                for (var i = 0; i < inputs.length; i++) {
                    inputs[i].onclick = switchLayer;
                }
    
            });
    

    I can see the Code is working fine: enter image description here

    Hope this will fix the problem.