Use contour lines to display elevation levels.

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, DoubleSide } 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";

function bindNumericalDropDown(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLSelectElement)) {
    throw new Error(
      "invalid binding element: expected HTMLSelectElement, got: " +
        element.constructor.name,
    );
  }

  element.onchange = () => {
    onChange(parseInt(element.value));
  };

  const callback = (v) => {
    element.value = v.toString();
    onChange(parseInt(element.value));
  };

  return [callback, parseInt(element.value), element];
}

function bindSlider(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.valueAsNumber);
  };

  const setValue = (v) => {
    element.valueAsNumber = v;
    onChange(element.valueAsNumber);
  };

  const initialValue = element.valueAsNumber;

  return [setValue, initialValue, element];
}

function bindToggle(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.checked);
  };

  const callback = (v) => {
    element.checked = v;
    onChange(element.checked);
  };

  return [callback, element.checked, element];
}

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

const extent = new Extent(
  "EPSG:3857",
  x - halfWidth,
  x + halfWidth,
  y - halfWidth,
  y + halfWidth,
);

const instance = new Instance({
  target: "view",
  crs: extent.crs,
});

const map = new Map({
  extent,
  hillshading: {
    enabled: true,
    intensity: 0.5,
  },
  side: DoubleSide,
  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.view.camera.position.set(-13594700, 5819700, 7300);

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

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

instance.view.setControls(controls);

instance.notifyChange();

Inspector.attach("inspector", instance);

bindToggle("contourLineCheckbox", (state) => {
  if (state) {
    document.getElementById("options").removeAttribute("disabled");
  } else {
    document.getElementById("options").setAttribute("disabled", "disabled");
  }
  map.contourLines.enabled = state;
  instance.notifyChange(map);
});

bindNumericalDropDown("mainInterval", (v) => {
  map.contourLines.interval = v;
  instance.notifyChange(map);
});
bindNumericalDropDown("secondaryInterval", (v) => {
  map.contourLines.secondaryInterval = v;
  instance.notifyChange(map);
});
bindSlider("opacitySlider", (v) => {
  map.contourLines.opacity = v;
  instance.notifyChange(map);
});
bindSlider("thicknessSlider", (v) => {
  map.contourLines.thickness = v;
  instance.notifyChange(map);
});
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="Use contour lines to display elevation levels."
    />
    <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/next/css/example.css"
    />
  </head>

  <body>
    <div id="view" class="m-0 p-0 w-100 h-100"></div>
    <div
      id="inspector"
      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": "git+https://gitlab.com/giro3d/giro3d.git"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}