x | ||
° | ||
° |
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 'ol/ol.css';
import Map from 'ol/Map';
import View from 'ol/View';
import {OSM, XYZ} from 'ol/source';
import {WebGLTile as TileLayer} from 'ol/layer';
const variables = {};
// The method used to extract elevations from the DEM.
// In this case the format used is
// red + green * 2 + blue * 3
//
// Other frequently used methods include the Mapbox format
// (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
// and the Terrarium format
// (red * 256 + green + blue / 256) - 32768
function elevation(xOffset, yOffset) {
return [
'+',
['*', 256, ['band', 1, xOffset, yOffset]],
[
'+',
['*', 2 * 256, ['band', 2, xOffset, yOffset]],
['*', 3 * 256, ['band', 3, xOffset, yOffset]],
],
];
}
// 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', ['^', ['+', ['^', dzdx, 2], ['^', dzdy, 2]], 0.5]];
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 XYZ({
url: 'https://{a-d}.tiles.mapbox.com/v3/aj.sf-dem/{z}/{x}/{y}.png',
}),
style: {
variables: variables,
color: ['color', scaled, scaled, 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();
const listener = function () {
updateValues();
shadedRelief.updateStyleVariables(variables);
};
control.addEventListener('input', listener);
control.addEventListener('change', listener);
});
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM(),
}),
shadedRelief,
],
view: new View({
extent: [-13675026, 4439648, -13580856, 4580292],
center: [-13615645, 4497969],
minZoom: 10,
maxZoom: 16,
zoom: 13,
}),
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shaded Relief (with WebGL)</title>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://unpkg.com/elm-pep@1.0.6/dist/elm-pep.js"></script>
<!-- The lines below are only needed for old environments like Internet Explorer and Android 4.x -->
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch,requestAnimationFrame,Element.prototype.classList,TextDecoder"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.18.3/minified.js"></script>
<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 src="main.js"></script>
</body>
</html>
{
"name": "webgl-shaded-relief",
"dependencies": {
"ol": "6.14.1"
},
"devDependencies": {
"parcel": "^2.0.0"
},
"scripts": {
"start": "parcel index.html",
"build": "parcel build --public-url . index.html"
}
}