Long and short: Trying to add 100+ Graphics to a GraphicsOverlay takes like 10 seconds. Changing from "each new Graphic gets a Symbol" to "The GraphicsOverlay uses a SimpleRenderer, and each Graphic gets its Symbol from the Renderer" resulted in marginal improvement at best. Are we doing this incorrectly?
This is a followup from https://community.esri.com/t5/net-maps-sdk-questions/adding-a-large-number-of-graphics-in-a-mvvm/m-p...
In our situation, we have a ViewModel that sits in a left-sidebar, and the rest of the application window is the map. The left sidebar holds all of the visible features on the map (limited to ~100) in a Vertical Listview. Each feature is Selectable, and when the feature is Selected, the feature in the Map is "highlighted" by drawing a Symbol over the feature.
On the map, we have a many features - often several thousand at a 25000:1 scale. These features are nested in 3 layers of Grandparent > Parent > Child relationships - unrelated to this issue, aside from a recursive selection method. These features can be Polyline, Point, MultiPoint, Envelope, Polygon.
Our sidebar has a Select All/None button, self explanatory. However, this involves us adding/removing 100+ graphics w/ symbols to/from the mapview's graphics overlays. As described at the top, this action is taking 8-10s when executed.
Switching to using a SimpleRenderer didn't provide noticeable improvements in execution. What are we doing wrong?
Our code:
// This guy gets fired on ViewModel instantiation
private void InitGraphicsLayers()
{
// `this.Engine` is just a DI wrapper class that holds the ArcGISRuntime MapView
this.pointGraphicsOverlay = this.Engine.MapView.GraphicsOverlays.FirstOrDefault(g => g.Id.Equals(PointGraphicsOverlayId));
if (this.pointGraphicsOverlay is null)
{
// straightforward, a Cyan circle over the symbology of the Point geometry
Symbol pointSymbol = new SimpleMarkerSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleMarkerSymbolStyle.X,
Size = 15d,
};
SimpleRenderer renderer = new(pointSymbol);
// we have a couple of constants in this class so we can always
// create and find our overlays in `this.Engine.MapView.GraphicsOverlays`
this.pointGraphicsOverlay = new GraphicsOverlay() { Id = PointGraphicsOverlayId, Renderer = renderer, };
this.Engine.MapView.GraphicsOverlays.Add(this.pointGraphicsOverlay);
}
this.polyGraphicsOverlay = this.Engine.MapView.GraphicsOverlays.FirstOrDefault(g => g.Id.Equals(PolyGraphicsOverlayId));
if (this.polyGraphicsOverlay is null)
{
// dashed cyan line, used for Line geometries as well
Symbol polySymbol = new SimpleFillSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleFillSymbolStyle.Null,
Outline = new SimpleLineSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleLineSymbolStyle.Dash,
Width = 5d,
},
};
SimpleRenderer renderer = new(polySymbol);
this.polyGraphicsOverlay = new GraphicsOverlay() { Id = PolyGraphicsOverlayId, Renderer = renderer, };
this.Engine.MapView.GraphicsOverlays.Add(this.polyGraphicsOverlay);
}
this.lineGraphicsOverlay = this.Engine.MapView.GraphicsOverlays.FirstOrDefault(g => g.Id.Equals(LineGraphicsOverlayId));
if (this.lineGraphicsOverlay is null)
{
Symbol outline = new SimpleLineSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleLineSymbolStyle.Dash,
Width = 5d,
};
SimpleRenderer renderer = new(outline);
this.lineGraphicsOverlay = new GraphicsOverlay() { Id = LineGraphicsOverlayId, Renderer = renderer, };
this.Engine.MapView.GraphicsOverlays.Add(this.lineGraphicsOverlay);
}
}
public void SetHighlight()
{
try
{
// local flag that checks if any of Line/Poly/Point graphics overlays are null
if (IsHighlightLayerNull)
{
this.InitGraphicsLayers();
}
// Remove all graphics
this.pointGraphicsOverlay.Graphics.Clear();
this.polyGraphicsOverlay.Graphics.Clear();
this.lineGraphicsOverlay.Graphics.Clear();
// Collect all selected Polygon features and return an IEnumerable<Graphic>
var polyGraphics =
this.SelectedFeatures
.Where(static feature => new List<GeometryType>() { GeometryType.Polygon, GeometryType.Envelope }.Contains(feature.Geometry.GeometryType))
.Select(static feature => new Graphic(feature.Geometry));// CreateGraphic(feature.Geometry));
// add our IEnumerable<Graphic> of polygons to the polyGraphicsOverlay
this.polyGraphicsOverlay.Graphics.AddRange(polyGraphics);
var lineGraphics =
this.SelectedFeatures
.Where(static feature => feature.Geometry.GeometryType == GeometryType.Polyline)
.Select(static feature => new Graphic(feature.Geometry));// CreateGraphic(feature.Geometry));
this.lineGraphicsOverlay.Graphics.AddRange(lineGraphics);
var pointGraphics =
this.SelectedFeatures
.Where(static feature => new List<GeometryType>() { GeometryType.Point, GeometryType.Multipoint }.Contains(feature.Geometry.GeometryType))
.Select(static feature => new Graphic(feature.Geometry));// CreateGraphic(feature.Geometry));
this.pointGraphicsOverlay.Graphics.AddRange(pointGraphics);
}
catch (Exception ex)
{
this.LogException(ex);
}
}
// This function currently isn't being used, but I included it for completeness.
// You can see in the `SetHighlight` method that we WERE calling this to create our Graphics
// but we switched it to use a SimpleRenderer instead
private static Graphic CreateGraphic(Geometry geometry)
{
// Create a graphic to display the specified geometry.
Symbol symbol = null;
SimpleLineSymbol outline = new SimpleLineSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleLineSymbolStyle.Dash,
Width = 5d,
};
switch (geometry.GeometryType)
{
// Symbolize with a fill symbol.
case GeometryType.Envelope:
case GeometryType.Polygon:
{
symbol = new SimpleFillSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleFillSymbolStyle.Null,
Outline = outline,
};
break;
}
// Symbolize with a line symbol.
case GeometryType.Polyline:
{
symbol = outline;
break;
}
// Symbolize with a marker symbol.
case GeometryType.Point:
case GeometryType.Multipoint:
{
symbol = new SimpleMarkerSymbol
{
Color = System.Drawing.Color.Cyan,
Style = SimpleMarkerSymbolStyle.X,
Size = 15d,
};
break;
}
}
// Pass back a new graphic with the appropriate symbol.
return new Graphic(geometry, symbol);
}
Hello Andrew!
GraphicsOverlay is normally able to handle hundreds of thousands of Graphics, so 100 Graphics should not be a problem. I am surprised to see this take 10 seconds, unless each feature's geometry is very large and complex.
You are definitely going in the right direction by switching from individually-assigned Symbols to a Renderer. That's one of the first things we suggest in our Graphics performance topic: Performance considerations | ArcGIS Maps SDK for .NET | ArcGIS Developers
The only obvious inefficiency I see in the code you shared is the way GeometryType is checked -- it creates 200 Lists just to check 100 features. It can be simplified to feature.Geometry.GeometryType is GeometryType.Polygon or GeometryType.Envelope, though this unlikely to make a big impact.
Your best bet would be to use a profiler to see how the applications spends these 10 seconds. It might point to a solution right away, or at least give us more information to find the performance bottleneck.
Thanks for the catch on the lists, I'll tighten that up, but I concur that I don't think that is really the root of the problem. I'm benchmarking currently but may have to shift to traces to see what's taking so long. I'm sure I'll find no async/onpropertychanged surprises in this super complicated MVVM app, not a one.