Custom LERC Layer

Explore in the sandboxView live

This sample demonstrates how to create a custom tile layer that displays elevation data requested from the ArcGIS world elevation service. Elevation changes are displayed in different shades of yellow. The brighter and stronger the color, the higher the elevation. Any elevation values higher than 6000 meters are colored with a complete yellow color.

Creating a custom tile layer

To create a custom Tilelayer you must call createSubclass() on BaseTileLayer. We'll name the custom layer LercLayer with properties for setting the minimum elevation, maximum elevation, and the url template for requesting each elevation tile.

         
const LercLayer = BaseTileLayer.createSubclass({
  properties: {
    urlTemplate: null,
    minElevation: 0,
    maxElevation: 4000
  }

  //override required methods
});

The fetchTile() method of LercLayer must be overridden so we can process raw elevation values and assign different shades of yellow to each pixel based on the elevation value. Once the url for a tile is returned, we can request the elevation data for that tile using esriRequest.

Since the world elevation service uses Esri LERC format, we must decode the data using the LercDecode.decode() method. This method returns a simple array of elevations. Once we have elevation values for each pixel, we can draw elevation on the canvas by assigning RGBA values to the imageData.

                                                                               
// fetch tiles visible in the view
fetchTile: function (level, row, col, options) {
  const url = this.getTileUrl(level, row, col);

  // requested encoded elevation information
  // the signal option ensures that obsolete requests are aborted
  return esriRequest(url, {
    responseType: "array-buffer",
    signal: options && options.signal
  })
  .then((response) => {
    // create a canvas to draw the processed image
    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");
    const width = this.tileInfo.size[0];
    const height = this.tileInfo.size[1];

    canvas.width = width;
    canvas.height = height;

    // uncompress raw elevation values in lerc format
    // into a pre-allocated array of elevation values.
    const lerc = LercDecode.decode(response.data, { noDataValue: 0 });

    // Array of elevation values width*height.
    const pixels = lerc.pixels[0];

    // stats for min, max and no data
    // values for uncompressed elevation
    const stats = lerc.statistics[0];

    // set the min and max elevation values set
    // by the layer
    const min = this.minElevation;
    const max = this.maxElevation;
    const noDataValue = stats.noDataValue;

    // Create a new blank image data object with the specified
    // dimensions.
    const imageData = context.createImageData(width, height);

    // get Uint8ClampedArray representing a one-dimensional array
    // containing the data in the RGBA order, with integer values
    // between 0 and 255 (included).
    const data = imageData.data;

    const factor = 256 / (max - min);
    let value = 0;
    let j;

    // Loop through elevation array from lerc to generate an
    // image that will be displayed.
    // `pixels` is a flat array of color values and alpha
    // [r, g, b, a, r, g, b, a, ...]
    // We need to iterate through elevation and assign color
    // and alpha values respectively.
    for (let i = 0; i < width * height; i++) {
      // map tile size is 256x256. Elevation values have a
      // tile size of 257 so we skip the last elevation
      // whenever "i" is incremented by 256 to jump to the next row.
      j = i + Math.floor(i / width);
      // read the elevation value at the given index from the elevation
      // array and multiply it by the factor. This will define
      // the shade of yellow color for the pixel.
      value = (pixels[j] - min) * factor;

      // create RGBA value for the pixel
      data[i * 4] = value;
      data[i * 4 + 1] = value;
      data[i * 4 + 2] = 0;
      data[i * 4 + 3] = pixels[i] === noDataValue ? 0 : value;
    }

    // The elevation change image and ready for display
    context.putImageData(imageData, 0, 0);

    return canvas;
  });
}