Custom Elevation Layer with Exaggeration

1600
4
Jump to solution
09-27-2017 01:57 AM
RichardReinicke
Occasional Contributor II

I`ve a question regarding using Custom Elevation Layers with exaggeration in local scenes. I`m generally following the following Esri example:
Custom ElevationLayer - Exaggerating elevation | ArcGIS API for JavaScript 4.4 

I`ve a working scene with an elevation layer and the elevation layer and scene seems to work fine. 

Check out the code on Plunker

If I try to replace the not exaggerated elevation layer 'elevLayer' with the exaggerated one 'secondElevLyr', then I get an error:

I`m not totally sure if our elevation service is already visible to the internet but maybe someone has a few hints for me how to check the tiling scheme mismatch?


TSolow-esristaff‌, do you maybe have an idea? You already helped me with a previous problem

0 Kudos
1 Solution

Accepted Solutions
YannCabon
Esri Contributor

The BaseElevationLayer indeed defines default values for fullExtent, spatialReference and tileInfo. To create your custom layer in a different SR or tiling scheme, in the loading chain you can load the original layer and then copy the values to the instance of the custom layer. I modified the layer from ArcGIS API for JavaScript Sandbox  to do this:

      var ExaggeratedElevationLayer = BaseElevationLayer.createSubclass({

        // Add an exaggeration property whose value will be used
        // to multiply the elevations at each tile by a specified
        // factor. In this case terrain will render 100x the actual elevation.

        properties: {
          exaggeration: 100
        },

        // The load() method is called when the layer is added to the map
        // prior to it being rendered in the view.

        load: function() {
          this._elevation = new ElevationLayer({
            url: "//elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"
          });

          // wait for the elevation layer to load before resolving load()
          this.addResolvingPromise(this._elevation.load().then(function() {
            this.fullExtent = this._elevation.fullExtent;
            this.spatialReference = this._elevation.spatialReference;
            this.tileInfo = this._elevation.tileInfo;
          }.bind(this)));
        },

        // Fetches the tile(s) visible in the view
        fetchTile: function(level, row, col) {
          // calls fetchTile() on the elevationlayer for the tiles
          // visible in the view
          return this._elevation.fetchTile(level, row, col)
            .then(function(data) {

              var exaggeration = this.exaggeration;

              // `data` is an object that contains the
              // the width of the tile in pixels,
              // the height of the tile in pixels,
              // and the values of each pixel
              data.values.forEach(function(value, index, values) {
                // each value represents an elevation sample for the
                // given pixel position in the tile. Multiply this
                // by the exaggeration value
                values[index] = value * exaggeration;
              });

              return data;
            }.bind(this));
        }
      });‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

The part that copies:

          // wait for the elevation layer to load before resolving load()
          this.addResolvingPromise(this._elevation.load().then(function() {
            this.fullExtent = this._elevation.fullExtent;
            this.spatialReference = this._elevation.spatialReference;
            this.tileInfo = this._elevation.tileInfo;
          }.bind(this)));

View solution in original post

4 Replies
ThomasSolow
Occasional Contributor III

Your elevation layer does seem to be private, but searching for that error in the source code tells me that it gets thrown when:

elevationLayer.tileInfo.spatialReference !== view.spatialReference

Looking at the BaseElevationLayer class, it seems that the spatial reference is always set to web mercator for the layer and for the tileInfo property.  I don't know why this is (I think it's just an assumption they made, that users are mainly going to create BaseElevationLayers to use the Esri world elevation layer, which is in web mercator), but you might try manually setting the SRs in the load function:

var ExaggeratedElevationLayer = BaseElevationLayer.createSubclass({
  ...,

  load: function() {
    const customSR = new SpatialReference({ wkid: 31463 });
    this._elevation = new ElevationLayer({
      spatialReference: customSR,
      url: "http://rips-rasterdaten.lubw.bwl.de/arcgis/rest/services/Imageservices/DGM025_cache_3D/ImageServer"
    });
    // wait for the elevation layer to load before resolving load()

    this.spatialReference = customSR;
    this.tileInfo.spatialReference = customSR;
    this._elevation.tileInfo.spatialReference = customSR;

    this.addResolvingPromise(this._elevation.load());
  },

  ...
});‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

This may not be enough, you may need to set the entire tileInfo object on this._elevation and/or on the base elevation layer itself.  Assuming the elevation layer works when it's not exaggerated, you may be able to copy tileInfo from the normal layer and hardcode it in the load function.

There's probably an easier way to do this by modifying the constructor but you may be able to get this to work.

RichardReinicke
Occasional Contributor II

Hello Thomas Solow‌,

thank you again for your help. I`m following your recommendation. Now I 

1. add the working not exaggerated ElevationLayer with an event handler added

elevLayer.on("layerview-create", function(event){
  tileInfo = event.target.tileInfo;
  var secondElevLyr = new ExaggeratedElevationLayer();
  map.ground.layers.add(secondElevLyr);
}.bind(this));‍‍‍‍‍‍

  I`m storing the tileInfo in a global variable cause I don`t know any other way to get in into the constructor load function. Not beautiful but it works for now.

2. In the load function of my second elevation layer I set the tileInfo as you suggested 

load: function() {
  const customSR = new SpatialReference({ wkid: 31463 });
  this._elevation = new ElevationLayer({
    spatialReference: customSR,
    url: "http://rips-rasterdaten.lubw.bwl.de/arcgis/rest/services/Imageservices/DGM025_cache_3D/ImageServer"
  });

  this.spatialReference = customSR;
  this.tileInfo = tileInfo;
  this._elevation.tileInfo = tileInfo;
 
  this.addResolvingPromise(this._elevation.load());
}


Now in Chrome Dev Tools beside some other errors I get another strange message. It tells me that the root scale leve of our tiling scheme which is 1:2.048.000 is to large for layers extent oO. Sure my SceneView is clipped but I expect the layer to choose the appropriate tile level !? Furthermore it suggests me an even bigger root scale of 1:600.000.000 which makes even less sense to me.



So is there something wrong with our tiling scheme? It`s our common tiling scheme and we use it for all services of our environmental agency. It`s the same like in the base layer which should be public and visible to you.

0 Kudos
ThomasSolow
Occasional Contributor III

I don't think there's anything wrong with your tiling scheme as it seems to work fine for your basemap and for elevation that isn't exaggerated.  I think the problem is definitely in the source code: BaseElevationLayer makes some assumptions about which kind of elevation layer it's modifying.

I'd try something like this:

load: function() {

  this._elevation = new ElevationLayer({
    spatialReference: tileInfo.spatialReference,
    tileInfo: tileInfo,
    fullExtent: fullExtent,
    url: "http://rips-rasterdaten.lubw.bwl.de/arcgis/rest/services/Imageservices/DGM025_cache_3D/ImageServer"
  });
 
  this.addResolvingPromise(this._elevation.load());
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

...

elevLayer.on("layerview-create", function(event){
  tileInfo = event.target.tileInfo;
  fullExtent = event.target.fullExtent;

  var secondElevLyr = new ExaggeratedElevationLayer({
    tileInfo: tileInfo,
    fullExtent: fullExtent,
    spatialReference: tileInfo.spatialReference
  });
  map.ground.layers.add(secondElevLyr);
}.bind(this));‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Setting tileInfo in the constructors might fix things, and it may be necessary to take full extent from the tiled service as well.  My feeling is that the layer is getting into a state where some properties are set for web mercator (due to that being hardcoded) and some are set for 31463.

In principle this should definitely be doable.  The normal elevation layer is working, all we're really doing is using the normal layer and tweaking the values a bit.

0 Kudos
YannCabon
Esri Contributor

The BaseElevationLayer indeed defines default values for fullExtent, spatialReference and tileInfo. To create your custom layer in a different SR or tiling scheme, in the loading chain you can load the original layer and then copy the values to the instance of the custom layer. I modified the layer from ArcGIS API for JavaScript Sandbox  to do this:

      var ExaggeratedElevationLayer = BaseElevationLayer.createSubclass({

        // Add an exaggeration property whose value will be used
        // to multiply the elevations at each tile by a specified
        // factor. In this case terrain will render 100x the actual elevation.

        properties: {
          exaggeration: 100
        },

        // The load() method is called when the layer is added to the map
        // prior to it being rendered in the view.

        load: function() {
          this._elevation = new ElevationLayer({
            url: "//elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"
          });

          // wait for the elevation layer to load before resolving load()
          this.addResolvingPromise(this._elevation.load().then(function() {
            this.fullExtent = this._elevation.fullExtent;
            this.spatialReference = this._elevation.spatialReference;
            this.tileInfo = this._elevation.tileInfo;
          }.bind(this)));
        },

        // Fetches the tile(s) visible in the view
        fetchTile: function(level, row, col) {
          // calls fetchTile() on the elevationlayer for the tiles
          // visible in the view
          return this._elevation.fetchTile(level, row, col)
            .then(function(data) {

              var exaggeration = this.exaggeration;

              // `data` is an object that contains the
              // the width of the tile in pixels,
              // the height of the tile in pixels,
              // and the values of each pixel
              data.values.forEach(function(value, index, values) {
                // each value represents an elevation sample for the
                // given pixel position in the tile. Multiply this
                // by the exaggeration value
                values[index] = value * exaggeration;
              });

              return data;
            }.bind(this));
        }
      });‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

The part that copies:

          // wait for the elevation layer to load before resolving load()
          this.addResolvingPromise(this._elevation.load().then(function() {
            this.fullExtent = this._elevation.fullExtent;
            this.spatialReference = this._elevation.spatialReference;
            this.tileInfo = this._elevation.tileInfo;
          }.bind(this)));