CARTO + Mapbox GL JS

CARTO + Mapbox GL JS

Getting started

Mapbox provides different SDKs for developing web and mobile (iOS and Android) applications. These SDKs include different visualization capabilities and you can learn more about them in the Mapbox Docs website.

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 JS map with a CARTO dataset!

Basic setup

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. For more info about this, you can read our blogpost Our Thoughts as MapboxGL JS v2.0 Goes Proprietary.

In this example we are going to use v2 to showcase new available functionality like the 3D elevated terrain capabilities but, if you want to use a library compatible with Mapbox 1.x and CARTO, community supported, we recommend you to check MapLibre.

We are going to start by adding a map with 3D terrain. Please check this example in the Mapbox docs. It uses exaggeration to exaggerate the height of the terrain and adds a sky layer that is shown when the map is highly pitched.

Adding data from CARTO

In order to visualize a CARTO dataset, you need to fetch data from the CARTO platform. With CARTO Maps API v3 you can add two different types of sources: GeoJSON and tilesets (vector).

  • For adding a source with GeoJSON format you first set your credentials and then you can use the getData function from the CARTO module for deck.gl. Once you have retrieved the data, you can just add a new source to the map with geojson type.

  • For adding a tileset, you can need to provide the TileJSON URL when adding your vector source. This URL needs the connection name and the access token. Please check the tileset example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
deck.carto.setDefaultCredentials({
  apiBaseUrl: 'https://gcp-us-east1.api.carto.com',
  apiVersion: deck.carto.API_VERSIONS.V3,
  accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiI1YjI0OWE2ZCJ9.Y7zB30NJFzq5fPv8W5nkoH5lPXFWQP0uywDtqUg8y8c'
});

const data =  await deck.carto.getData({
  type: deck.carto.MAP_TYPES.QUERY,
  source: `SELECT geom FROM cartobq.public_account.grca_trans_trail_ln`,
  connection: 'bqconn',
  format: deck.carto.FORMATS.GEOJSON
});

map.addSource('trails', {
  'type': 'geojson',
  'data': data
})

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>