Js 10

  • uno

  • due

  • non

Mapbox Map parameters and methods

The mapboxgl Map Object can be used to:

Starting from a basic map:

new mapboxgl.Map({
      container: "map", 
      style: "mapbox://styles/mapbox/streets-v11", 
      center: [12, 42, 
      zoom: 9, 
});

We can include:

new mapboxgl.Map({
  ...
  antialias: true,    
  bearing: 20,          //initial degree rotation
  bearingSnap: 45,      //max degree rotation before map snap to normal
  clickTolerance: 20,   //the pixels that need to be swept to get a drag before being a click
  cooperativeGestures: true,  //the zoom will need the crtl button but doesn't stops double click to zoom
  doubleClickZoom: false,     //to disable double-click zoom
  dragPan: false,       //will disable the drag move map
  dragRotate: false,    //won't allow rotate nor pitch 3D
  interactive: false,   //will disable any movement or zoom
  maxZoom, minZoom, maxPitch, minPitch:   //for limits (max) or immediate(min)
  pitchWithRotate: false      //will disable pitch 
})

We use projection to change the view of the map:

Cover

projection: "mercator" will create a continous map

Cover

projection: "albers" will give us a conic projection

Cover

projection: "equalEarth" will have rounded border

The map event listeners can include Layers

Starting source
frap.on('load', () => {

    frap.addSource('places1', {
        'type': 'geojson',
        'data': {
          'type': 'FeatureCollection',
          'features': [
              {
                'type': 'Feature',
                'properties': {
                  'description': '<h2>la montagna</h2>'
                },
                'geometry': {
                  'type': 'Point',
                  'coordinates': [13, 45.2]
                }
              }
          ]
        }
    })
  
//we will use the ID later
    frap.addLayer({
      'id': 'places',
      'type': 'circle',
      'source': 'places1',
      'paint': {
        'circle-color': '#ff64fb',
        'circle-radius': 6,
        'circle-stroke-width': 3,
        'circle-stroke-color': '#ffffff'
      }
    });
  
})

We can have:

First for the mouse movements in/out the map:

//when mouse gets out 
mappe.on("mouseout", ()=>{
  console.log( "MOUSE fuori" )
})

//only once when mouse gets into map
mappe.on("mouseover", ()=>{
  console.log("MOUSE dentro")
})

//each frame of movement IN the map
mappe.on("mousemove" , ()=>{
  console.log("MOUSE mosso")
})

We also have event listeners for when the map is dragged/moved:

//at the start/middle/end of the map movement

mappe.on("movestart", ()=>{
  console.log("MAPPA comincia mossa")
})

mappe.on("move", ()=>{
  console.log("MAPPA mossa")
})

mappe.on("moveend", ()=>{
  console.log("MOUSE smesso mossa")
})

For the Event to trigger on the Layer elements we:

//we use the Layer ID, to include more layers we ["places", "more"]

mappe.on("mouseover", "places", ()=>{
  console.log("MOUSE dentro")
})

We can then use its event features[0].properties:

//on click on map we can have map coordinates and canvas point position 

mappe.on("click", (e)=>{
    console.log(e.point)                              //{x: 572, y: 233}
    console.log(e.lngLat+ e.lngLat.lng+ e.lngLat.lat) //{lng: -92.0058, lat: 45.0721}
})

Querying features of Mapbox layers

We can use queryRenderedFeatures() to get an array of GeoJSON Feature objects from the layer we are on, using visible features that satisfy the query parameters (geometry):

Filter feature properties of a clicked query Point
mappe.on('click', (e)=>{
  let band= mappe.queryRenderedFeatures(e.point, {
    layers: ["circle"]
  })

//we create an array of filter properties, like the source used for the layer
  const limited = [
    'properties',
    'source',
    'layer'
  ];
  
  const finito = band.map((feat) => {
    const tutto = {};
    
    //we create limited properties to the new object, using the original[prop]
    limited.forEach((prop) => {
      tutto[prop] = feat[prop];
    });

    return tutto;
  })

  //we get returned an object that we need to stringify
  console.log( JSON.stringify( finito, null, 2 ) )
  //the string will have 2 empty spaces
})
On click() we filter the query specific features

We can include the Query Features in the event listener:

// this is the same as 
// band= mappe.queryRenderedFeatures(e.point, { layers: ["state-fills"] })

frap.on("mousemove", "state-fills", (e)=>{

//and the e argument works only if the select layer is listened to
    e.features[0].properties.STATE_NAME == band[0].properties.STATE_NAME

    console.log( e.lngLat )    //will get the coordinates in the layer
    console.log( e.point )     //will get the position IN THE MAP X/Y width/height
})
Coordinates and Pont position

We can also change the cursor if in a Layer using getCanvas():

//we can use feature.length or just E for the if statement
mappe.getCanvas().style.cursor = features.length ? 'pointer' : '';

An example with query features and layers:

Hover on Layer event
Paint layers and Hover query feature events

On load we:

//we need a variable for the current hovered
let hoveredStateId = null;

mappe.on('load', () => {

  frap.addSource('stati', {
    'type': 'geojson',
    'data': 'https://docs.mapbox.com/mapbox-gl-js/assets/us_states.geojson'
  });

//we separate line BORDERS from FILL
  frap.addLayer({
    'id': 'state-borders',
    'type': 'line',
    'source': 'stati',
    'layout': {},
    'paint': {
      'line-color': '#c80815',
      'line-width': 2
    }
  });

//because of the feature-state dependent fill-opacity expression
  frap.addLayer({
    'id': 'state-fills',
    'type': 'fill',
    'source': 'stati',
    'layout': {},
    'paint': {
      'fill-color': '#627BC1',
      'fill-opacity': [
        'case',
        ['boolean', ['feature-state', 'hover'], false],
        1,
        0.5
      ]
    }
  });


  frap.on("mousemove", "state-fills", (e)=>{

  //to update the hover effect on event
    if (hoveredStateId ) {
      frap.setFeatureState(
        { source: 'stati', id: hoveredStateId },
        { hover: false }
      );
    }
  
  //we set ID on current hover ID
    hoveredStateId = e.features[0].id;
      
  //we set the current ID as the hover paint effect
    frap.setFeatureState(
      { source: 'stati', id: hoveredStateId },
      { hover: true }
    );
  //we can also add Popups and stuff here
    
  })

//we also add event for when out of the query
  frap.on('mouseleave', 'state-fills', () => {

    frap.getCanvas().style.cursor = '';

    if (hoveredStateId !== null) {
      frap.setFeatureState(
        { source: 'stati', id: hoveredStateId },
        { hover: false }
      );
    }

    hoveredStateId = null;
    pop.remove()
  });

})

We can also mix queryRenderedFeatures() and Layers:

we select the Layer features objects by the query
How to draw expandible square on canvas mapboxgl

First, we need the CSS for the box:

.boxdraw {
    background: rgba(56, 135, 190, 0.1);
    border: 2px solid #3887be;
    position: absolute;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
}

Then we need the Canvas :

map6.on("load", (e)=>{
  let canvas= map6.getCanvasContainer();

  let start;
  let current;
  let box;
  
  canvas.addEventListener('mousedown', mouseDown)

//on click down AND shift we start getting the position of the box
  function mouseDown(e) {
    if (!(e.shiftKey && e.button === 0)) return;

    map6.dragPan.disable();
  //we disable the drag to keep the map still and get start position mousePos()
    start = mousePos(e);
  
  //both to update and finish the box
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  }

  function mousePos(e){
    const rect = canvas.getBoundingClientRect();

  //on canvas, clientXY distance from top-left 0/0 we return width/height box mapbox Object
    return new mapboxgl.Point(
      e.clientX - rect.left - canvas.clientLeft,
      e.clientY - rect.top - canvas.clientTop
    );
  }

//on mousemove we have
  
  function onMouseMove(e) {
  //current moving XY canvas coordinates
    current = mousePos(e);

  // Append the box element if not yet
    if (!box) {
      box = document.createElement('div');
      box.classList.add('boxdraw');
      canvas.appendChild(box);
    }
    
    const 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);
         
  //we translate the 0/0 boxdraw with the minXY and its width/height with maxXY
    const pos = `translate(${minX}px, ${minY}px)`;
    box.style.transform = pos;

    box.style.width = maxX - minX + 'px';
    box.style.height = maxY - minY + 'px';
  }
  
//we create the diagonal query with the start and ending point position on canvas
  function onMouseUp(e) {
    finish( [start, mousePos(e)] );
  }

//we remove the box events on mouse, so we dont stretch the box
  function finish(bbox) {
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);

    map6.dragPan.enable();
  }
  
})

We are gonna use the [start, mousePos(e)] next

For the Souce and filtered layers we:

map3.addSource('counties', {
  'type': 'vector',
  'url': 'mapbox://mapbox.82pkq93d'
});

//fill inside colors
map3.addLayer(
  {
  'id': 'counties',
  'type': 'fill',
  'source': 'counties',
  'source-layer': 'original',
  'paint': {
    'fill-outline-color': 'rgba(0,200,0,0.3)',
    'fill-color': 'rgba(200,0,0,0.1)'
  }
  },
  'settlement-label'
); 

//we add a FILTER for the layer on hover
map3.addLayer(
  {
  'id': 'counties-highlighted',
  'type': 'fill',
  'source': 'counties',
  'source-layer': 'original',
  'paint': {
    'fill-outline-color': '#484896',
    'fill-color': 'rgba(200,0,0,0.8)',
    'fill-opacity': 0.75
  },
  'filter': ['in', 'FIPS', '']
  },
  'settlement-label'
);

Now that we got a query features area we:

//being bbox 2 X/Y mapboxgl point objects forming the diagonal of a square

function finish(bbox) {
  document.removeEventListener('mousemove', onMouseMove);
  document.removeEventListener('mouseup', onMouseUp);
  
  if (box) {
  //mapboxgl-canvas-container is parent of boxdraw, like mapboxgl-canvas
  //we remove if present to just get the hover layer  
    box.parentNode.removeChild(box);
    box = null;
  }

  if (bbox){
  //we do the same as with points 
    const features = map3.queryRenderedFeatures(bbox, {
      layers: ['counties']
    });
  //if number selected within the box exceeds
    if (features.length >= 1000) {
      return window.alert('Select a smaller number of features');
    }
  
  //between the selected on the layer we return the FIPS and use them in the filter
    const fips = features.map((feature) => feature.properties.FIPS);
  
    map3.setFilter('counties-highlighted', ['in', 'FIPS', ...fips]);
  }

}
Instead of box on drag we use 2 click boxes
Custom 2 points box for query feature events

We will need 2 arrays, one for the markers on coordinates and another for the canvas points:

// Some code

map5.on("load", (e)=>{

  let canvas= map5.getCanvasContainer();
  let box;

  let markers= []
  let points= []
  
  let start;
  let endi;
  
  map2.on("click", (e)=>{
   //if we already have 2 points we delete the box and points
    if( points.length== 2 ){
      points= []
      for( x of markers){    
        x.remove()
      }
      box.parentNode.removeChild(box);
      box = null;
    }
  
    points.push( e.point )
    
    let uno= new mapboxgl.Marker(pick)
      .setLngLat( e.lngLat )
      .addTo(map2)
    markers.push( uno )

    if( points.length == 2 ){
     //when 2 points we use canvas for the box
      start= points[0]
      endi= points[1]

      box = document.createElement('div');
      box.classList.add('boxdraw');
      canvas.appendChild(box);

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

      const pos = `translate(${minX}px, ${minY}px)`;
      box.style.transform = pos;

      box.style.width = maxX - minX + 'px';
      box.style.height = maxY - minY + 'px';
    
      finish([start, endi])

    }
  })

  map2.on("move", (e)=>{
   //after the box or points, any move will clear them
    if(points.length> 0 ){
      points= []
      for( x of markers ){    
        x.remove()
      }
      if(box){
        box.parentNode.removeChild(box);
        box = null;
      }
    }

  })
//In this example we used a done style with its layer pointarea

function finish(bbox){
  const features = map2.queryRenderedFeatures(bbox, {
    layers: ['pointarea']
  });

  console.log( features )
}

QuerySourceFeatures() returns an array of GeoJSON Feature objects from its source that satisfy the query parameter:

Instead of using an area in Features we use the source to filter
Filter features containing using its data

The source is similar but we use a different property:

map3.addSource('counties', {
  'type': 'vector',
  'url': 'mapbox://mapbox.82pkq93d'
});

map3.addLayer(
  {
  '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)'
    }
  },
  // Place this layer under labels, roads and buildings.
  'building'
);

map3.addLayer(
  {
  'id': 'counties-highlighted',
  'type': 'fill',
  'source': 'counties',
  'source-layer': 'original',
  'paint': {
    'fill-outline-color': '#aa4896',
    'fill-color': '#c80015',
    'fill-opacity': 0.75
  },
  // Display none by adding a
  // filter with an empty string.
  'filter': ['in', 'COUNTY', '']
  },
  'building'
);

We use querySourceFeatures() on the layer:

//to get the filter effect we just need

map3.on('mousemove', 'counties', (e) => {

  const feature = e.features[0];

  map3.setFilter('counties-highlighted', [
    'in',
    'COUNTY',
    feature.properties.COUNTY
  ]);

 //To get the actual data from the filtered we need
  const relatedCounties = map3.querySourceFeatures('counties', {
    sourceLayer: 'original',
    filter: ['in', 'COUNTY', feature.properties.COUNTY]
  });
  //we select complete features based on the selected property
  
 //querySourceFeatures can return repeated features so we can filter them
  const uniqueCounties = getUniqueFeatures(relatedCounties, 'FIPS');

 //we can sum the values of the filtered properties
  const populationSum = uniqueCounties.reduce((memo, feature) => {
    return memo + feature.properties.population;
  }, 0);

})

We use set() to not repeat features on getUniqueFeatures():

//we return  a set of features 

function getUniqueFeatures(features, comparatorProperty) {
  const uniqueIds = new Set();
  const uniqueFeatures = [];

  for (const feature of features) {
    const id = feature.properties[comparatorProperty];

    if (!uniqueIds.has(id)) {
      uniqueIds.add(id);
      uniqueFeatures.push(feature);
    }
  }

  return uniqueFeatures;
}

We can also create a 3D terrain map using mapbox-dem:

we can also exagerate the heigths
var map4 = new mapboxgl.Map({
  ...
  pitch: 80,
  bearing: 41,
  style: 'mapbox://styles/mapbox/satellite-streets-v12'
})

map4.on('load', () => {

  map4.addSource('mapbox-dem', {
    'type': 'raster-dem',
    'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
    'tileSize': 512,
    'maxzoom': 14  
  });

  //exaggeration from 1 to 1000 and not on buildings
  map4.setTerrain({ 
    'source': 'mapbox-dem', 
    'exaggeration': 2
  });

});

Last updated

Was this helpful?