Skip to content

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 using the “Hide temperatures below” slider.

In the image below, you can see how the same data looks when the Scene viewingMode is set to global.

custom-temp-layer-globe

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 the variation at large scales
factor: 80000,
},
load: function () {
// Load ImageryLayer containing temperature values
// See the code in the sandbox for details about
// createTemperatureLayer()
this._temperature = createTemperatureLayer(this.depth, this.date, "lerc");
this.addResolvingPromise(this._temperature.load());
},
});

The createTemperatureLayer() 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 pixels 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. fetchTile() must return a promise that resolves an object of type ElevationTileData.

fetchTile: function (level, row, col, options) {
const bounds = this.getTileBounds(level, row, col);
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;
if (options.signal?.aborted) return Promise.reject(new Error("aborted"));
// Fetch the pixels 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.fetchPixels(extent, tileSize, tileSize, options).then((data) => {
if (options.signal?.aborted) throw new Error("aborted");
const pixelBlock = data.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) => {
// Multiply temperatures by the given factor.
// Areas with no temperature data (land) will be assigned the average sea surface temperature (17 degrees Celsius)
pixelData[index] = value !== noDataValue ? value * factor : 17 * factor;
});
// Return the modified temperatures as elevations
return { values: elevations, width: pixelBlock.width, height: pixelBlock.height, noDataValue: noDataValue };
});
}

The Temperature3DLayer is added as ground layer of the Scene component’s map. 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 scene
// with a 2D ImageryLayer representing elevation draped on top
viewElement.map = new Map({
basemap: "oceans",
ground: {
layers: [createTemperature3DLayer(selectedDepth, selectedDate)],
},
layers: [createTemperature2DLayer(selectedDepth, selectedDate)],
});