Getting started

In this guide, you will learn the basics of visualizing a CARTO dataset with the Mapbox GL JS library. There are no previous requirements to complete this guide, but a basic knowledge of the Mapbox GL JS library would be helpful.

After completing this guide, you will have your first Mapbox GL map with a CARTO dataset!

The following example is built on top of Mapbox GL JS v2. It enables 3D mapping with elevated terrain, customizable skies and camera functionalities.

This example adds 3D terrain to a map using setTerrain with a raster-dem source.

It uses exaggeration to exaggerate the height of the terrain. It also adds a sky layer that is shown when the map is highly pitched.

About MapboxGL license

Beginning with v2.0.0, mapbox-gl-js is no longer under the 3-Clause BSD license. Also, from that same version, a billable map load occurs whenever a Map object is initialized. That leaves 1.13.0 as the latest mapbox-gl-js version with BSD, that can be freely used.

If you want to continue using the mapbox-gl free version (remember that 3D elevated terrain capabilities start from v2), use the following CARTO CDN:

1
2
  <script src="https://libs.cartocdn.com/mapbox-gl/v1.13.0/mapbox-gl.js"></script>
  <link href="https://libs.cartocdn.com/mapbox-gl/v1.13.0/mapbox-gl.css" rel="stylesheet" />

For more info about this, you can read our blogpost Our Thoughts as MapboxGL JS v2.0 Goes Proprietary.

Basic setup

The first thing you need to do is to add all the required Mapbox GL dependencies (library and CSS files):

1
2
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>

Add map container

Next, you need to create a div inside the body where the map will be drawn and you need to style them to ensure the map displays at full width:

1
2
3
<body style="margin: 0; padding: 0;">
  <div id="map" style="position: absolute; top: 0; bottom: 0; width: 100%;"></div>
</body>

Create map and set properties

Once you have a div for your map, you can use the mapboxgl.Map constructor to create your map with the desired initial view. Here we are also specifying the style property to use a raster-dem source. You will need to provide your Mapbox access token:

1
2
3
4
5
6
7
8
const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y',
  center: [-112.125, 36.12],
  zoom: 12,
  pitch: 70,
  bearing: 180
});

At this point you will have a basic 3D map with terrain:

Add layer

In order to visualize a CARTO dataset, you just need to provide a TileJSON URL using the Maps API within a source of type vector while you are creating your layer using the addLayer method on the map. We also need to indicate the ID for the layer and the styling properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const query = 'SELECT the_geom_webmercator FROM grca_trans_trail_ln';
const tilejsonUrl = `https://maps-api-v2.us.carto.com/user/public/carto/sql?source=${query}&format=tilejson&api_key=default_public&rand=3435334`;
map.addLayer({
  'id': 'grca-trail-layer',
  'type': 'line',
  'source': {
    'type': 'vector',
    url: tilejsonUrl
  },
  'source-layer': 'default',
  'paint': {
    'line-color': 'orange',
    'line-width': 3
  }
});

In order to have the best performance, we recommend you to retrieve only the fields you want to use client-side, instead of selecting all the fields (SELECT *). If you select all the fields from the dataset, the vector tiles will be bigger than needed and would take more time to encode, download and decode.

Camera and animation

The example uses the new Free Camera API in Mapbox GL JS v2 and features an initial animation that updates the camera position using linear interpolation between two locations.

All together

You can explore the final step here

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
    <script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>
  </head>
  <body style="margin: 0; padding: 0;">
    <div id="map" style="position: absolute; top: 0; bottom: 0; width: 100%;"></div>
  </body>
  <script>
    mapboxgl.accessToken = 'pk.eyJ1IjoiY2FydG9kYmluYyIsImEiOiJja202bHN2OXMwcGYzMnFrbmNkMzVwMG5rIn0.Zb3J4JTdJS-oYNXlR3nvnQ';

    async function initialize() {
      const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y',
        center: [-112.125, 36.12],
        zoom: 12,
        pitch: 70,
        bearing: 180
      });

      map.on('load', async () => {
        map.addSource('mapbox-dem', {
          'type': 'raster-dem',
          'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
          'tileSize': 512,
          'maxzoom': 14
        });

        // add the DEM source as a terrain layer with exaggerated height
        map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.5 });
        
        // add a sky layer that will show when the map is highly pitched
        map.addLayer({
          'id': 'sky',
          'type': 'sky',
          'paint': {
            'sky-type': 'atmosphere',
            'sky-atmosphere-sun': [0.0, 0.0],
            'sky-atmosphere-sun-intensity': 15
          }
        });

        // Add CARTO layer
        const query = 'SELECT the_geom_webmercator FROM grca_trans_trail_ln';
        const tilejsonUrl = `https://maps-api-v2.us.carto.com/user/public/carto/sql?source=${query}&format=tilejson&api_key=default_public&rand=3435334`;
        map.addLayer({
          'id': 'grca-trail-layer',
          'type': 'line',
          'source': {
            'type': 'vector',
            url: tilejsonUrl
          },
          'source-layer': 'default',
          'paint': {
            'line-color': 'orange',
            'line-width': 3
          }
        });
      });
    }

    initialize();

    function updateCameraPosition(position, altitude, target) {
      const camera = map.getFreeCameraOptions();
      camera.position = mapboxgl.MercatorCoordinate.fromLngLat(
        position,
        altitude
      );
      camera.lookAtPoint(target);
      
      map.setFreeCameraOptions(camera);
    }

    // linearly interpolate between two positions based on time
    function lerp(a, b, t) {
      if (Array.isArray(a) && Array.isArray(b)) {
        const result = [];
        for (let i = 0; i < Math.min(a.length, b.length); i++) {
          result[i] = a[i] * (1 - t) + b[i] * t;
        }
        return result;
      } else {
        return a * (1 - t) + b * t;
      }
    }

    let animationTime = 0.0;

    function animate() {
      const animationConfig = {
        duration: 1000,
        animate: (phase) => {
          const start = [-112.125, 36.3];
          const end = [-112.125, 36.05];
            
          // interpolate camera position while keeping focus on a target lat/lng
          const position = lerp(start, end, phase);
          const altitude = 5000;
          const target = [-112.065, 36.147];
            
          updateCameraPosition(position, altitude, target);
        }
      };
      
      let lastTime = 0.0;
      let frameReq;
      function frame(time) {
        if (animationTime < animationConfig.duration) {
          animationConfig.animate(animationTime / animationConfig.duration);
        }
        
        // allow requestAnimationFrame to control the speed of the animation
        animationTime += 100 / (time - lastTime);
        lastTime = time;
        
        if (animationTime > animationConfig.duration) {
          window.cancelAnimationFrame(frameReq);
          return;
        }
        
        frameReq = window.requestAnimationFrame(frame);
      }
      
      frameReq = window.requestAnimationFrame(frame);
    };
  </script>
</html>