Custom ElevationLayer - Thematic data as elevation

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.

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 because it is a loadable resource.

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.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 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 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. fetchTile() must return a promise that resolves to an object specified in ElevationTileData.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 Temperature3DLayer 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.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
// 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()]
});

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.