A map with elevation contour lines

Main interval (m)
Secondary interval (m)

Contour lines can be enabled on Map entities to visualize relief. Change line intervals, opacity, thickness and color, as well as opacity. Note that on very flat surfaces, contour lines can produce artifacts where their displayed thickness is greater than desired.

index.js
import colormap from 'colormap';
import { Color } from 'three';
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
import XYZ from 'ol/source/XYZ.js';

import Extent from '@giro3d/giro3d/core/geographic/Extent.js';
import Instance from '@giro3d/giro3d/core/Instance.js';
import ElevationLayer from '@giro3d/giro3d/core/layer/ElevationLayer.js';
import Map from '@giro3d/giro3d/entities/Map.js';
import GeoTIFFFormat from '@giro3d/giro3d/formats/GeoTIFFFormat.js';
import ColorMap, { ColorMapMode } from '@giro3d/giro3d/core/layer/ColorMap.js';
import TiledImageSource from '@giro3d/giro3d/sources/TiledImageSource.js';
import Inspector from '@giro3d/giro3d/gui/Inspector.js';



const x = -13602000;
const y = 5812000;
const halfWidth = 2500;

// Defines geographic extent: CRS, min/max X, min/max Y
const extent = new Extent('EPSG:3857', x - halfWidth, x + halfWidth, y - halfWidth, y + halfWidth);

const viewerDiv = document.getElementById('viewerDiv');

const instance = new Instance(viewerDiv, { crs: extent.crs() });

const map = new Map('planar', {
    extent,
    hillshading: {
        enabled: true,
        intensity: 0.5,
    },
    segments: 128,
    doubleSided: true,
    backgroundColor: 'white',
    contourLines: true,
});

instance.add(map);

const source = new TiledImageSource({
    source: new XYZ({
        minZoom: 10,
        maxZoom: 16,
        url: 'https://3d.oslandia.com/dem/MtStHelens-tiles/{z}/{x}/{y}.tif',
    }),
    format: new GeoTIFFFormat(),
});

const floor = 1100;
const ceiling = 2500;

const values = colormap({ colormap: 'viridis', nshades: 256 });
const colors = values.map(v => new Color(v));

const dem = new ElevationLayer({
    name: 'dem',
    source,
    extent,
    colorMap: new ColorMap(colors, floor, ceiling, ColorMapMode.Elevation),
});

map.addLayer(dem);

instance.camera.camera3D.position.set(-13594700, 5819700, 7300);

const controls = new MapControls(instance.camera.camera3D, instance.domElement);

controls.target.set(-13603000, 5811000, 0);

instance.useTHREEControls(controls);

instance.notifyChange();


Inspector.attach(document.getElementById('panelDiv'), instance);

const checkbox = document.getElementById('contourLineCheckbox');
checkbox.oninput = function oninput() {
    const state = checkbox.checked;
    map.materialOptions.contourLines.enabled = state;
    instance.notifyChange(map);
};

function bindSlider(name, fn) {
    const slider = document.getElementById(name);
    slider.oninput = function oninput() {
        fn(slider.value);
        instance.notifyChange(map);
    };
}

function bindDropDown(name, fn) {
    const mode = document.getElementById(name);
    mode.onchange = () => {
        fn(Number.parseFloat(mode.value));
        instance.notifyChange(map);
    };
}

bindDropDown('mainInterval', v => {
    map.materialOptions.contourLines.interval = v;
});
bindDropDown('secondaryInterval', v => {
    map.materialOptions.contourLines.secondaryInterval = v;
});
bindSlider('opacitySlider', v => {
    map.materialOptions.contourLines.opacity = v;
});
bindSlider('thicknessSlider', v => {
    map.materialOptions.contourLines.thickness = v;
});
index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Giro3D - Contour lines</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
  <style>
    body {
      padding: 0;
      margin: 0;
      width: 100vw;
      height: 100vh;
    }

    #viewerDiv {
      width: 100%;
      height: 100%;
    }

    #panelDiv {
      position: absolute;
      top: 0;
      left: 0;
    }
    
  </style>
</head>

<body>
  <div id="viewerDiv"></div>
  <div id="panelDiv"></div>
  <div class="m-2 position-absolute top-0 end-0">
    <!-- Hillshading -->
    <div class="card m-1">
        <div class="card-header">
            <div class="form-check form-switch">
                <input
                    class="form-check-input"
                    type="checkbox"
                    checked="true"
                    role="switch"
                    id="contourLineCheckbox"
                />
                <label class="form-check-label" for="contourLineCheckbox">Contour lines</label>
            </div>
        </div>

        <fieldset class="container m-1 p-2">
            <div class="input-group">
                <span class="input-group-text flex-grow-1">Main interval (m)</span>
                <select class="btn btn-outline-primary btn-sm" id="mainInterval">
                    <option value="0">Disabled</option>
                    <option selected value="100">100 m</option>
                    <option value="200">200 m</option>
                    <option value="400">400 m</option>
                </select>
            </div>

            <div class="my-2"></div>

            <div class="input-group">
                <span class="input-group-text flex-grow-1">Secondary interval (m)</span>
                <select class="btn btn-outline-primary btn-sm" id="secondaryInterval">
                    <option value="0">Disabled</option>
                    <option selected value="10">10 m</option>
                    <option selected value="20">20 m</option>
                    <option value="50">50 m</option>
                </select>
            </div>

            <div class="my-2"></div>

            <label for="opacitySlider" class="form-label">Opacity</label>
            <div class="input-group">
                <input
                    type="range"
                    min="0"
                    max="1"
                    value="1"
                    step="0.05"
                    class="slider"
                    id="opacitySlider"
                />
            </div>

            <label for="thicknessSlider" class="form-label">Thickness</label>
            <div class="input-group">
                <input
                    type="range"
                    min="0"
                    max="2"
                    value="1"
                    step="0.05"
                    class="slider"
                    id="thicknessSlider"
                />
            </div>
        </fieldset>
    </div>
</div>

  <script type="module" src="index.js"></script>
  <script>
    /* activate popovers */
    const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
    popoverTriggerList.map(
      // bootstrap is used as script in the template, disable warning about undef
      // eslint-disable-next-line no-undef
      popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl, {
        trigger: 'hover',
        placement: 'left',
        content: document.getElementById(popoverTriggerEl.getAttribute('data-bs-content')).innerHTML,
        html: true,
      }),
    );
  </script>
</body>

</html>
package.json
{
  "name": "contour_lines",
  "dependencies": {
    "@giro3d/giro3d": "0.35.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}