---
title: "Add Contour Lines. | JavaScript maps SDK"
description: "Add map contour lines generated from a raster-dem source."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Add Contour Lines.

This example shows how to add contour lines to your map from a raster-dem source. In this particular case, the contour lines are generated dynamically on the client side, specifically in the browser, through the utilization of the maplibre-contour plugin.

If you require accurate and precise contour lines, it is advisable to utilize the data derived from the [Global Contours offered by MapTiler](https://cloud.maptiler.com/tiles/contours/).

The Global Contours, created by MapTiler, contains contour lines for locations all around the globe in two widely used unit systems: meters and feet. With its global coverage, MapTiler’s contour lines can be utilized for various purposes, such as outdoor recreation planning, landscape analysis, and cartographic visualization.

### NPM module setup

```bash
npm install --save @maptiler/sdk
```

#### main.js

```js
import { Map, config } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';

config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';

const map = new Map({
  container: 'map',
  style: DATAVIZ.LIGHT, 
  zoom: 13.42,
  center: [-148.63567, 62.03381],
});

const demSource = new mlcontour.DemSource({
  url: `https://api.maptiler.com/tiles/terrain-rgb-v2/{z}/{x}/{y}.webp?key=${config.apiKey}`,
  encoding: 'mapbox',
  maxzoom: 12,
  // offload contour line computation to a web worker
  worker: true
});

// calls maplibregl.addProtocol to register a dynamic vector tile provider that
// downloads raster-dem tiles, computes contour lines, and encodes as a vector
// tile for each tile request from maplibre
demSource.setupMaplibre(maptilersdk);

map.on('load', function () {
  map.addSource("hillshadeSource", {
    "type": "raster-dem",
    // share cached raster-dem tiles with the contour source
    tiles: [demSource.sharedDemProtocolUrl],
    maxzoom: 14
  });

  map.addSource("contourSourceFeet", {
    type: 'vector',
    tiles: [
      demSource.contourProtocolUrl({
        // meters to feet
        multiplier: 3.28084,
        overzoom: 1,
        thresholds: {
          // zoom: [minor, major]
          11: [200, 1000],
          12: [100, 500],
          13: [100, 500],
          14: [50, 200],
          15: [20, 100]
        },
        elevationKey: 'ele',
        levelKey: 'level',
        contourLayer: 'contours'
      })
    ],
    maxzoom: 15
  });

  map.addLayer({
    id: 'hills',
    type: 'hillshade',
    source: 'hillshadeSource',
    layout: { visibility: 'visible' },
    paint: { 'hillshade-exaggeration': 0.25 }
  });

  map.addLayer({
    id: 'contours',
    type: 'line',
    source: 'contourSourceFeet',
    'source-layer': 'contours',
    paint: {
      'line-opacity': 0.5,
      // "major" contours have level=1, "minor" have level=0
      'line-width': ['match', ['get', 'level'], 1, 1, 0.5]
    }
  });

  map.addLayer({
    id: 'contour-text',
    type: 'symbol',
    source: 'contourSourceFeet',
    'source-layer': 'contours',
    filter: ['>', ['get', 'level'], 0],
    paint: {
      'text-halo-color': 'white',
      'text-halo-width': 1
    },
    layout: {
      'symbol-placement': 'line',
      'text-size': 10,
      'text-field': [
        'concat',
        ['number-format', ['get', 'ele'], {}],
        '\''
      ],
      'text-font': ['Noto Sans Bold']
    }
  });

});
```

#### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex">
  <title>Add Contour Lines. | NPM example</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="map"></div>
  <script type="module" src="./main.js"></script>
</body>
</html>
```

#### style.css

```css
body {margin: 0; padding: 0;}
#map {position: absolute; top: 0; bottom: 0; width: 100%;}
```

### Basic JavaScript setup

```html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Contour Lines</title>
<script src="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.umd.min.js"></script>
<link href="https://cdn.maptiler.com/maptiler-sdk-js/v4.2.0/maptiler-sdk.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-contour@0.0.5/dist/index.min.js"></script>
<style>
  body {margin: 0; padding: 0;}
  #map {position: absolute; top: 0; bottom: 0; width: 100%;}
</style>
</head>
<body>
<div id="map"></div>

<script>
    maptilersdk.config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';

    const map = new maptilersdk.Map({
      container: 'map',
      style: DATAVIZ.LIGHT, 
      zoom: 13.42,
      center: [-148.63567, 62.03381],
    });

    const demSource = new mlcontour.DemSource({
      url: `https://api.maptiler.com/tiles/terrain-rgb-v2/{z}/{x}/{y}.webp?key=${maptilersdk.config.apiKey}`,
      encoding: 'mapbox',
      maxzoom: 12,
      // offload contour line computation to a web worker
      worker: true
    });

    // calls maplibregl.addProtocol to register a dynamic vector tile provider that
    // downloads raster-dem tiles, computes contour lines, and encodes as a vector
    // tile for each tile request from maplibre
    demSource.setupMaplibre(maptilersdk);

    map.on('load', function () {
      map.addSource("hillshadeSource", {
        "type": "raster-dem",
        // share cached raster-dem tiles with the contour source
        tiles: [demSource.sharedDemProtocolUrl],
        maxzoom: 14
      });

      map.addSource("contourSourceFeet", {
        type: 'vector',
        tiles: [
          demSource.contourProtocolUrl({
            // meters to feet
            multiplier: 3.28084,
            overzoom: 1,
            thresholds: {
              // zoom: [minor, major]
              11: [200, 1000],
              12: [100, 500],
              13: [100, 500],
              14: [50, 200],
              15: [20, 100]
            },
            elevationKey: 'ele',
            levelKey: 'level',
            contourLayer: 'contours'
          })
        ],
        maxzoom: 15
      });

      map.addLayer({
        id: 'hills',
        type: 'hillshade',
        source: 'hillshadeSource',
        layout: { visibility: 'visible' },
        paint: { 'hillshade-exaggeration': 0.25 }
      });

      map.addLayer({
        id: 'contours',
        type: 'line',
        source: 'contourSourceFeet',
        'source-layer': 'contours',
        paint: {
          'line-opacity': 0.5,
          // "major" contours have level=1, "minor" have level=0
          'line-width': ['match', ['get', 'level'], 1, 1, 0.5]
        }
      });

      map.addLayer({
        id: 'contour-text',
        type: 'symbol',
        source: 'contourSourceFeet',
        'source-layer': 'contours',
        filter: ['>', ['get', 'level'], 0],
        paint: {
          'text-halo-color': 'white',
          'text-halo-width': 1
        },
        layout: {
          'symbol-placement': 'line',
          'text-size': 10,
          'text-field': [
            'concat',
            ['number-format', ['get', 'ele'], {}],
            '\''
          ],
          'text-font': ['Noto Sans Bold']
        }
      });

    });

</script>
</body>
</html>
```

## Related examples

### [3D terrain map](https://docs.maptiler.com/sdk-js/examples/3d-map/)

Tutorials

Create and display 3D terrain maps on web pages.

### [Start with AR maps](https://docs.maptiler.com/sdk-js/examples/ar-control/)

Examples

Add an AR control button to map 3D viewports.

### [Switch between contour lines heights in meters and feet](https://docs.maptiler.com/sdk-js/examples/contour-meter-feet/)

Examples

Switch map contour line altitude units between meters and feet.
