This sample demonstrates how to create a custom ElevationLayer using thematic data. In this case the thematic variable is sea temperature. The warmer the temperature, the higher the elevation. The elevation of land areas represents the average sea surface temperature (~17° C), so the pseudo ocean elevations that peak above nearby land are areas where the temperature is greater than 17° C and vice versa. A white graphic is overlaid at this elevation to show temperatures greater than 17°. This graphic may be moved by sliding the handle of the Temperatures above ##°
slider.
Switch the viewingMode of the SceneView to global
to get a different view of the data.
Creating a custom elevation layer
To create a custom ElevationLayer, you must call createSubclass() on BaseElevationLayer.
Since this sample exaggerates thematic values from an ImageryLayer, we require ImageryLayer and load the ScientificData/SeaTemperature image service as a dependency of the custom layer. This is done inside the load() method.
Since the temperatures have a relatively small range (-2°C to 30°C), we'll need to multiply them by a large number so we can see enough contrast between elevations at small scales. We'll create a factor
property to specify this value.
const Temperature3DLayer = BaseElevationLayer.createSubclass({
properties: {
// exaggerates the temps by 80000x so we can see
// variation at large scales
exaggeration: 80000
},
load: function() {
// load ImageryLayer containing temperature values
// See the code in the sandbox for details about
// createTemperatureLayer()
this._temperature = createTemperatureLayer(depth, tempDate, "lerc");
this.addResolvingPromise(this._temperature.load());
}
});
The create
function returns an ImageryLayer visualizing temperature values for for a specific date and sea depth. We request the data in lerc
format so the value of each pixel represents a temperature value.
Then we must manipulate the fetchTile() method so that it returns new elevation values based solely on sea temperature. Therefore, we expect to see variation in z-values for pixels covering the oceans, but a constant value for land.
To position the elevation values properly, we can fetch images from the ImageryLayer using the extent properties returned from the getTileBounds() method. Once the image covering the area of the elevation tile is fetched, we can manipulate the values of each pixel so it can more closely resemble an elevation. The function iterates through each pixel and multiplies its value by the factor. If we encounter pixels covering land (noDataValues), then we'll set a constant elevation of the average sea temperature multiplied by the factor. fetch
must return a promise that resolves to an object specified in ElevationTileData.
fetchTile: function(level, row, col, options) {
const bounds = this.getTileBounds(level, row, col);
// Add one because of overlapping vertices in neighboring tiles
const tileSize = this.tileInfo.size[0] + 1;
const extent = new Extent({
xmin: bounds[0],
ymin: bounds[1],
xmax: bounds[2],
ymax: bounds[3],
spatialReference: this.spatialReference
});
const factor = this.factor;
// fetch the image representing temperature for the extent of the tile.
// this method returns the pixel data of the image for the extent
// of the given elevation tile
return this._temperature.fetchImage(extent, tileSize, tileSize, options)
.then((data) => {
const pixelBlock = data.pixelData.pixelBlock;
// contains the temperature values of each pixel in the image
const elevations = pixelBlock.pixels[0];
const stats = pixelBlock.statistics[0];
// pixels that don't contain any temperature values
const noDataValue = stats.noDataValue;
elevations.forEach((value, index, pixelData) => {
if (value !== noDataValue) {
// multiply temperatures by the given factor
pixelData[index] = value * factor;
}
else {
// areas with no temperature data (land)
// will be assigned the average sea surface
// temperature (17 degrees Celsius)
pixelData[index] = 17*factor;
}
});
// return the modified temperatures as elevations
return {
values: elevations,
width: pixelBlock.width,
height: pixelBlock.height,
noDataValue: noDataValue
};
});
}
Once the Temperature3
layer is created, you must add it to the layers of the Map.ground property and add the map to a SceneView instance. We also add a custom 2D tile layer draped on top of the surface to make the elevation data easier to see.
// Add the Temperature3DLayer as an elevation layer to the map
// with a 2D ImageryLayer representing elevation draped on top
const map = new Map({
basemap: "oceans",
ground: {
layers: [new Temperature3DLayer()]
},
layers: [new Temperature2DLayer()]
});