Calculate shaded relief from elevation data
For the shaded relief, a single tiled source of elevation data is used as input.
The shaded relief is calculated by the layer's style
with a color
expression. The style variables are updated when the user drags one of the sliders. The
band
operator is used to sample data from neighboring pixels for calculating slope and
aspect, which is done with the ['band', bandIndex, xOffset, yOffset]
syntax.
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import TileLayer from 'ol/layer/WebGLTile.js';
import ImageTile from 'ol/source/ImageTile.js';
import OSM from 'ol/source/OSM.js';
const variables = {};
// The method used to extract elevations from the DEM.
// In this case the format used is Terrarium
// red * 256 + green + blue / 256 - 32768
//
// Other frequently used methods include the Mapbox format
// (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
//
function elevation(xOffset, yOffset) {
const red = ['band', 1, xOffset, yOffset];
const green = ['band', 2, xOffset, yOffset];
const blue = ['band', 3, xOffset, yOffset];
// band math operates on normalized values from 0-1
// so we scale by 255
return [
'+',
['*', 255 * 256, red],
['*', 255, green],
['*', 255 / 256, blue],
-32768,
];
}
// Generates a shaded relief image given elevation data. Uses a 3x3
// neighborhood for determining slope and aspect.
const dp = ['*', 2, ['resolution']];
const z0x = ['*', ['var', 'vert'], elevation(-1, 0)];
const z1x = ['*', ['var', 'vert'], elevation(1, 0)];
const dzdx = ['/', ['-', z1x, z0x], dp];
const z0y = ['*', ['var', 'vert'], elevation(0, -1)];
const z1y = ['*', ['var', 'vert'], elevation(0, 1)];
const dzdy = ['/', ['-', z1y, z0y], dp];
const slope = ['atan', ['sqrt', ['+', ['^', dzdx, 2], ['^', dzdy, 2]]]];
const aspect = ['clamp', ['atan', ['-', 0, dzdx], dzdy], -Math.PI, Math.PI];
const sunEl = ['*', Math.PI / 180, ['var', 'sunEl']];
const sunAz = ['*', Math.PI / 180, ['var', 'sunAz']];
const cosIncidence = [
'+',
['*', ['sin', sunEl], ['cos', slope]],
['*', ['cos', sunEl], ['sin', slope], ['cos', ['-', sunAz, aspect]]],
];
const scaled = ['*', 255, cosIncidence];
const shadedRelief = new TileLayer({
opacity: 0.3,
source: new ImageTile({
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
maxZoom: 15,
attributions:
'<a href="https://github.com/tilezen/joerd/blob/master/docs/attribution.md" target="_blank">Data sources and attribution</a>',
}),
style: {
variables: variables,
color: ['color', scaled],
},
});
const controlIds = ['vert', 'sunEl', 'sunAz'];
controlIds.forEach(function (id) {
const control = document.getElementById(id);
const output = document.getElementById(id + 'Out');
function updateValues() {
output.innerText = control.value;
variables[id] = Number(control.value);
}
updateValues();
control.addEventListener('input', function () {
updateValues();
shadedRelief.updateStyleVariables(variables);
});
});
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM(),
}),
shadedRelief,
],
view: new View({
center: [-13615645, 4497969],
zoom: 13,
}),
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shaded Relief (with WebGL)</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}
table.controls td {
padding: 2px 5px;
}
table.controls td:nth-child(3) {
text-align: right;
min-width: 3em;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<table class="controls">
<tr>
<td><label for="vert">vertical exaggeration:</label></td>
<td><input id="vert" type="range" min="1" max="5" value="1"/></td>
<td><span id="vertOut"></span> x</td>
</tr>
<tr>
<td><label for="sunEl">sun elevation:</label></td>
<td><input id="sunEl" type="range" min="0" max="90" value="45"/></td>
<td><span id="sunElOut"></span> °</td>
</tr>
<tr>
<td><label for="sunAz">sun azimuth:</label></td>
<td><input id="sunAz" type="range" min="0" max="360" value="45"/></td>
<td><span id="sunAzOut"></span> °</td>
</tr>
</table>
<script type="module" src="main.js"></script>
</body>
</html>
{
"name": "webgl-shaded-relief",
"dependencies": {
"ol": "10.5.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}