Use contour lines to display elevation levels.
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.
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, min, max, step) => {
if (min != null && max != null) {
element.min = min.toString();
element.max = max.toString();
if (step != null) {
element.step = step;
}
}
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);
});
<!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/latest/examples/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>
{
"name": "contour_lines",
"dependencies": {
"colormap": "^2.3.2",
"@giro3d/giro3d": "0.40.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}