Select to view content in your preferred language

Questions for utilizing Spatial Analysis/Field Function tools

150
5
Jump to solution
yesterday
JBuchanan
Emerging Contributor

Hi I was just diving into the new Spatial Analysis tools added in 300.0 since I use raster data extensively in my line of work. I was doing some initial test to try and learn how to use the new map algebra functions and ran into a few issues. The only sample I could find was this Apply map algebra | ArcGIS Maps SDK for .NET | Esri Developer, if there are more samples I could look over, please let me know! 

Here are some issues/questions I've had so far:

1. I created a ContinuousFieldFunction with a few filters on it. I can export the results to file and load those files as rasters and that all works fine. I was wondering if I could apply the field function to an existing raster layer without needing to write the data to disk first? 

I was trying to find the sample from the gif on the Spatial Anaylsis page (Spatial analysis | ArcGIS Maps SDK for .NET | Esri Developer) since it states it uses map algebra operations to create the gif and im assuming each of those 'frames' is not individual raster files on disk?

 

2. I was trying to apply class breaks renderer to my raster. So, I have a List<ClassBreak> with each break having a color and upper value range. The idea was to reclassify the raster so that instead of floats id have int values for each break. I then apply a ColormapRenderer with the colors associated to each class break's index. 
I was trying something like this initially:

ContinuousField field = await ContinuousField.CreateAsync([path], 0);
ContinuousFiledFunction contFieldFunc = ContinuousFieldFunction.Create(field);
DiscreteFieldFunction discFunc = contFieldFunc.ToDiscreteFieldFunction();
for (int i = 0; i < classBreaks.Count; i++)
{
   BooleanFieldFunction boolFunc = contFieldFunc.IsLessThanOrEqualTo(classBreaks[i].Value);
   discFunc = discFunc.ReplaceIf(boolFunc, i);
}
DicreteField classBreaksField = await discFunc.EvaluateAsync();

 But I couldn't get it to work. It would only keep my last bool function. I also tried inverting the order of my class breaks so that something like "IsLessThanOrEqualTo(100)" wouldn't 'cover up' "IsLessThanOrEqualTo(50)", as an example. But neither worked. 
I could, however, get it working if I did all the functions in a single line like:

discFunc.ReplaceIf(boolFunc0, 0).ReplaceIf(boolFunc1, 1).ReplaceIf(boolFunc2, 2)... etc

 But I can't build out this single-line expression when the user specifies how many class breaks they want. Is it possible to chain functions together inside a loop?

 

3. DiscreteField & BooleanField classes state they support all raster pixel types except float (32-bit or 64-bit) up to 2147483647. I just want to clarify that this covers both signed and unsigned integers? 

4. Similar to #3, ContinuousField seems to only support 32-bit float rasters? My initial test was on a 64-bit float raster, and it threw an exception. I converted to 32-bit, and it worked. I'm just seeking clarification on the supported types for these two fields.

0 Kudos
1 Solution

Accepted Solutions
Nicholas-Furness
Esri Regular Contributor

Hi @JBuchanan.

Great to see you diving in to the new APIs.

We're a little thin on the ground with samples at the moment, but building more out as we can.

Some answers for you:

1. Outputting to an in-memory raster for use in a raster layer is something we know is an important capability, and is on the roadmap.

In terms of that sample, you're right, it's not writing out loads of rasters and loading them in. That's actually using the AnalysisOverlay. This sample shows how to do that (for a single overlay), but the principle is pretty simple: once you have your analysis defined, create a FieldAnalysis from it, and add that to an AnalysisOverlay in your MapView's AnalysisOverlays collection. Then just update the analysis parameters (anywhere in the analysis chain), and the display will update to reflect those changes.

In that example, from the DevTech Summit plenary, the code does this:

  1. Creates one AnalysisOverlay and adds it to the MapView's AnalysisOverlays collection.
  2. For each drone, it creates a ViewshedFunction with the drone's parameters (including location and heading). All these ViewshedFunctions use the same imported elevation ContinuousFieldFunction (as in your first code snippet).
  3. Create a FieldAnalysis for each ViewshedFunction, providing the same ColormapRenderer.
  4. Add all the FieldAnalysis objects to the Analyses collection of the AnalysisOverlay from step 1.

Now things are set up, the SDK takes over. As drone position updates come in, just update the parameters for the appropriate ViewshedFunction, and the SDK will handle the rest.

Note that there are currently a couple of limitations to be aware of:

  • Viewshed and Line of Sight functions don't yet work correctly in geographic coordinate systems, so this may give you unexpected results in a SceneView.
  • The spatial reference of the final analysis needs to match that of the GeoView. If your map is WebMercator, that usually just means tell the createAsync() call to project your elevation to WebMercator.

We're working hard on reducing these limitations.

One hint: as you test, keep an eye on the GeoView's AnalysisViewStateChanged event to help debug issues.

2. I'm not sure I can explain this one. I'm not a .NET developer, but what you're doing looks reasonable (especially since you've tried reversing the order). One note: when I've done this kind of reclassification, I've used a lower and upper bound (see the swift example halfway down this blog post). My initial thought is the SDK might be reducing/optimizing things internally in a way you're not expecting, but that doesn't explain why it works when chained in one line vs applied in an iteration. Someone smarter than I am will add their thoughts…

3. Signed 32-bit integers should behave as expected, with values in the range of -2147483648…2147483647, so consider that the same upper limit for both 32-bit signed and unsigned ints.

4. The CreateAsync documentation says "Supports reading of all raster pixel types except 64-bit float." Our target has been the pixel types listed as supported by Pro in this table, with the exceptions mentioned in the CreateAsync constructors.

I'll see if we can maybe be a bit more explicit about this.

[Updated to correct "projected" to "geographic" coordinate systems with respect to Viewshed/Line of Sight limitations]

View solution in original post

0 Kudos
5 Replies
Nicholas-Furness
Esri Regular Contributor

Hi @JBuchanan.

Great to see you diving in to the new APIs.

We're a little thin on the ground with samples at the moment, but building more out as we can.

Some answers for you:

1. Outputting to an in-memory raster for use in a raster layer is something we know is an important capability, and is on the roadmap.

In terms of that sample, you're right, it's not writing out loads of rasters and loading them in. That's actually using the AnalysisOverlay. This sample shows how to do that (for a single overlay), but the principle is pretty simple: once you have your analysis defined, create a FieldAnalysis from it, and add that to an AnalysisOverlay in your MapView's AnalysisOverlays collection. Then just update the analysis parameters (anywhere in the analysis chain), and the display will update to reflect those changes.

In that example, from the DevTech Summit plenary, the code does this:

  1. Creates one AnalysisOverlay and adds it to the MapView's AnalysisOverlays collection.
  2. For each drone, it creates a ViewshedFunction with the drone's parameters (including location and heading). All these ViewshedFunctions use the same imported elevation ContinuousFieldFunction (as in your first code snippet).
  3. Create a FieldAnalysis for each ViewshedFunction, providing the same ColormapRenderer.
  4. Add all the FieldAnalysis objects to the Analyses collection of the AnalysisOverlay from step 1.

Now things are set up, the SDK takes over. As drone position updates come in, just update the parameters for the appropriate ViewshedFunction, and the SDK will handle the rest.

Note that there are currently a couple of limitations to be aware of:

  • Viewshed and Line of Sight functions don't yet work correctly in geographic coordinate systems, so this may give you unexpected results in a SceneView.
  • The spatial reference of the final analysis needs to match that of the GeoView. If your map is WebMercator, that usually just means tell the createAsync() call to project your elevation to WebMercator.

We're working hard on reducing these limitations.

One hint: as you test, keep an eye on the GeoView's AnalysisViewStateChanged event to help debug issues.

2. I'm not sure I can explain this one. I'm not a .NET developer, but what you're doing looks reasonable (especially since you've tried reversing the order). One note: when I've done this kind of reclassification, I've used a lower and upper bound (see the swift example halfway down this blog post). My initial thought is the SDK might be reducing/optimizing things internally in a way you're not expecting, but that doesn't explain why it works when chained in one line vs applied in an iteration. Someone smarter than I am will add their thoughts…

3. Signed 32-bit integers should behave as expected, with values in the range of -2147483648…2147483647, so consider that the same upper limit for both 32-bit signed and unsigned ints.

4. The CreateAsync documentation says "Supports reading of all raster pixel types except 64-bit float." Our target has been the pixel types listed as supported by Pro in this table, with the exceptions mentioned in the CreateAsync constructors.

I'll see if we can maybe be a bit more explicit about this.

[Updated to correct "projected" to "geographic" coordinate systems with respect to Viewshed/Line of Sight limitations]

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Correction: I meant to say that Viewshed and Line of Sight don't work correctly in geographic coordinate systems. I have updated my reply above.

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Can you share more code for Q2 please? Specifically:

  • What are the class breaks you're using, and how are they defined? It doesn't look like you're using our `ClassBreak` class since it's designed around ranges with `minValue` and `maxValue`. I'm guessing you have some other classification data that simply defines an upper bound?
  • Can you also share the code that defines `boolFunc0`, `boolFunc1`, `boolFunc2` in your working chained example.

Where you're successively replacing by less than a larger and larger amount, we would expect the last replacement to consume previous ones (the set of things <600 is included in the set of things <800, so if you've replaced everything <600 with 0, then everything <800 with 1, you would not see any of the 0s, only the 1s). We can reproduce that.

But where we reverse that, so that we first replace things <800, then replace things <600, we see what we would expect (two separate categories).

We also can't reproduce anything different based off inline chaining vs iterating in a loop.

All that considered, if you could share your code, it would be really helpful. Thanks!

0 Kudos
JBuchanan
Emerging Contributor

Oops, I just went and tried the code using the loop again and it worked this time. I must have had a different error when I last tried it. Sorry for the confusion.

0 Kudos
JBuchanan
Emerging Contributor

Thanks for the quick and thorough response!


I looked into using AnalysisOverlays and that worked as you described. I did have issues with projection at first (which you already mentioned), but that was easily sorted. However, I don't believe the AnalysisOverlay is directly useable for most of my setup simply because it has no callout/identify.

The AnalysisOverlays would be most useful for me if I could connect it directly to a NetCDF. Many of the NetCDF I deal with have variables for time and depth/elevation and are mostly used for visual inspection of data in an area. So, if I could tie in some time/depth sliders to field functions connected to an AnalysisOverlay which visually streams the data directly from my NetCDF then that would be incredibly useful. The current approach would be to export the data from the NetCDF into 1000s of TIFFs for each combination of time/depth and then manually toggle the visibility/set active layer from among the collection which would be quite slow due to map refresh.


I am still exploring some capability and so far, everything seems to work great. I mostly use the ExportToFilesAsync() call for when i'm calculating a new raster. 

 

The only raster capability that I am still struggling to implement is a class breaks renderer. Prior to 300 I was manually reclassifying my rasters and exporting the result as a new TIFF. I then have 2 TIFFs for each raster layer. One for visualization and the other for the data with the 'visual' raster being the layer actually on the map. When the user clicks a cell on the 'visual' raster, I then extract the true value from that coord in the 'data' raster instead. If I were to use FieldFunctions/AnalysisOverlays for this, I would essentially get the same behavior where I would need extract the true value from the original data myself rather than relying on the built in identify tools. So, I think for Class Breaks I'm better off keeping what I have until either a dedicated ClassBreaksRenderer is added for Rasters, or once the in-memory raster capability from the roadmap is added.

 

Thanks again for your help, the map algebra stuff is incredibly useful and has great potential!

0 Kudos