A map with elevation contour lines

Main interval (m)
Secondary interval (m)
100% © U.S. Geological Survey

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,
    },
    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;
    if (state) {
        document.getElementById('options').removeAttribute('disabled');
    } else {
        document.getElementById('options').setAttribute('disabled', 'disabled');
    }
    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>
        <title>Contour lines</title>
        <meta charset="UTF-8" />
        <meta name="name" content="contour_lines" />
        <meta name="description" content="A map with elevation contour lines" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <link rel="icon" href="https://giro3d.org/images/favicon.svg" />
        <link href="https://giro3d.org/assets/bootstrap-custom.css" rel="stylesheet" />
        <script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
        <link
            rel="stylesheet"
            type="text/css"
            href="https://giro3d.org/latest/css/example.css"
        />

        
    </head>

    <body>
        <div id="viewerDiv" class="m-0 p-0 w-100 h-100"></div>
        <div id="panelDiv" class="position-absolute top-0 start-0 mh-100 overflow-auto"></div>

        <div class="side-pane-with-status-bar">
            <div class="card">
                <div class="card-header">
                    <div class="form-check form-switch">
                        <input
                            class="form-check-input"
                            type="checkbox"
                            checked="true"
                            role="switch"
                            id="contourLineCheckbox"
                            autocomplete="off"
                        />
                        <label class="form-check-label" for="contourLineCheckbox">Contour lines</label>
                    </div>
                </div>

                <div class="card-body">
                    <fieldset class="container" id="options">
                        <div class="input-group my-2">
                            <span class="input-group-text flex-grow-1">Main interval (m)</span>
                            <select
                                class="btn btn-outline-primary btn-sm"
                                id="mainInterval"
                                autocomplete="off"
                            >
                                <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="input-group my-2">
                            <span class="input-group-text flex-grow-1">Secondary interval (m)</span>
                            <select
                                class="btn btn-outline-primary btn-sm"
                                id="secondaryInterval"
                                autocomplete="off"
                            >
                                <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>

                        <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="form-range"
                                id="opacitySlider"
                                autocomplete="off"
                            />
                        </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="form-range"
                                id="thicknessSlider"
                                autocomplete="off"
                            />
                        </div>
                    </fieldset>
                </div>
            </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": {
        "colormap": "^2.3.2",
        "@giro3d/giro3d": "0.37.2"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}