ProConcepts GraphicsLayers - kataya/arcgis-pro-sdk GitHub Wiki
Graphics layers are layers that act as containers for graphic elements - points, lines, polygons, text, or images - that can be added to a map. Graphics elements provide a way to include simple annotations on a map or layout that highlight particular areas or points of interest.
Language: C#
Subject: Map Authoring
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 11/24/2020
ArcGIS Pro: 2.7
Visual Studio: 2017, 2019
- Background
- Creating Graphics Layer
- Accessing Graphics Layer
- Add Elements
- Select and UnSelect Elements
- ElementSelectionChangedEvent
- Group and Ungroup Elements
- Remove Elements
- Copy Elements
- Z-Order
- MoveElements
Graphics layers are a special kind of layer designed to work with graphic elements. Graphics elements can contain a geometry or a picture and provide a way to include a simple annotation or highlight on a given map. Graphics elements can be used to visualize a map without the need to create features. Graphics elements derive from ArcGIS.Desktop.Layout.GraphicElement and are identical in their implementation to graphics elements added to a layout for similar purposes.
Each element in a graphics layer will have its own unique name across all graphics layers in the map. A graphic element's name acts as the primary key for all the elements within a map (or layout). Their visibility is controlled by the graphics layer. If the coordinate system of the map container changes, graphics elements will remain in their assigned geographic position. However, because graphic elements are not features, they do not have associated attributes and cannot be queried.
As of 2.6 graphics layers are supported in 2D only. Multipatch geometry is not supported at 2.6. When dealing with multipatches, developers should use the existing mapview add overlay methods instead.
To create a GraphicsLayer use the LayerFactory.Instance.CreateLayer<T>()
templated "flavor", topic26527.html, and set the templated type to be GraphicsLayer
. For the layer creation params use GraphicsLayerCreationParams. Graphics layers, same as other layers, can be added directly to the TOC or as a child of a group layer. By default, Graphics layers are added to the top of the TOC unless otherwise specified. Their drawing order is controlled by their position in the TOC also the same as other layer (this is different than the behavior of graphics in the map overlay which always renders on top of all other map content). In the following example we are adding a graphics layer called "Graphics Layer" to the currently active map. We are checking the map type to ensure it is 2D.
var map = MapView.Active.Map;
if (map.MapType != MapType.Map)
return;// not 2D
var gl_param= new GraphicsLayerCreationParams { Name = "Graphics Layer" };
QueuedTask.Run( () => {
//By default will be added to the top of the TOC
var grahicsLayer = LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, map);
//or add to the bottom of the TOC
//LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, map,
// LayerPosition.AddToBottom);
//or add a graphics layer to a group layer...
//var group_layer = map.GetLayersAsFlattenedList().OfType<GroupLayer>().First();
//LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, group_layer);
//TODO...use the graphics layer
});
Note: The created graphics layer will have the spatial reference of the map.
Graphics layers are retrieved from the Map's layers collection in the same way as other layers. Use map.GetLayersAsFlattenedList()
and cast the returned layers to be .OfType<GraphicsLayer>()
or search for a specific Graphics Layer by uri or name using map.FindLayer(...)
and map.FindLayers(...)
respectively.
//get the first graphics layer in the map's collection of graphics layers
var grahicsLayer = map.GetLayersAsFlattenedList().OfType<GraphicsLayer>().FirstOrDefault();
if (grahicsLayer != null) {
...
The "TargetGraphicsLayer" is a special property on the map. It points to the graphics layer that will act as the target for the interactive drawing tools (used to create new map graphics). If the TargetGraphicsLayer has not been set when the Graphics tab is activated on the UI, it will default to the selected graphics layer in the TOC. If no graphics layer is selected in the TOC, it will default to the first Graphics Layer (in the TOC). To set it programatically, retrieve the desired graphics layer instance from the map and set it as the TargetGraphicsLayer: map.TargetGraphicsLayer = graphicsLayer;
. Users can interactively change the TargetGraphicsLayer via the "Target layer:" combo on Graphics tab.
Like all layers, Graphics Layers contain a CIM Definition, in this case CIMGraphicsLayer
. The CIM Definition, however, does not contain the graphics elements. For optimization reasons, the graphics themselves are stored in what is called "Element Storage", an external binary file managed by native (cpp) code associated with the layer. The "normal" layer properties, such as visibility, selectablity, minimum and maximum viewing scales, names and so forth are accessible via the CIM (in addition to the managed properties and "set" methods on the model).
Graphics layer provides a number of AddElement overloads for adding elements as CIMGraphics. Point, multi-point, line, envelope, polygon, text, and picture graphics can currently be added to a graphics layer. Multipatch geometry is currently not supported.
To add an element either provide the "raw" geometry and a (optional) symbol or create the CIMGraphic first within add-in code. Internally, when a geometry is provided, the graphics layer itself creates a CIMGraphic using the symbol that was provided or a default if the provided symbol was null. When a graphics element is created it is given a unique name - unique across all element content in all layers for the given map - and its top-level parent is set as the graphics layer to which it was added. The returned GraphicElement will contain the original CIMGraphic that was used to create it.
The geometry of the graphics elements should always be in the same spatial reference as the map (containing the graphics layer).
var graphicsLayer = MapView.Active.Map.GetLayersAsFlattenedList()
.OfType<ArcGIS.Desktop.Mapping.GraphicsLayer>().FirstOrDefault();
if (graphicsLayer == null)
return;
//place a point graphic in the middle of the map
QueuedTask.Run(() => {
var extent = MapView.Active.Extent;
var location = extent.Center;
//use the default symbology (a black circle)
var ge = graphicsLayer.AddElement(location);
var cimGraphic = ge.GetGraphic() as CIMPointGraphic;//get the contained CIMGraphic
//specify a symbol
var pt_symbol = SymbolFactory.Instance.ConstructPointSymbol(
ColorFactory.Instance.GreenRGB);
graphicsLayer.AddElement(location, pt_symbol);
//create a CIMGraphic
var graphic = new CIMPointGraphic() {
Symbol = pt_symbol.MakeSymbolReference(),
Location = location
};
graphicsLayer.AddElement(graphic);
...
Note: there are two limits for adding elements to a graphics layer (or layout):
- A maximum graphics count of 4000 elements (to include group elements) per map
- A total combined size limit of 10MB per map (meaning the combined size of all elements cannot exceed the size limit)
Whichever limit is hit first (if a limit is hit) will result in a InValidOperationException
. Elements will have to be removed at that point before more can be added.
Each GraphicElement
contains the original CIMGraphic
used to create it. To access and update the contained CIMCGraphic use the GraphicElement.GetGraphic(), SetGraphic(cimGraphic) method pair. For example, use the contained graphic to retrieve all text elements within a graphics layer:
var all_text = graphicsLayer.GetElementsAsFlattenedList()
.Where(e => e.GetGraphic() is CIMTextGraphic);
Changes can be made to symbology and/or geometry of the graphic element and applied back via SetGraphic.
//within a queued Task
//get the first line element in the layer
var ge = graphicsLayer.FindElement("Line 10") as GraphicElement;
var graphic = ge.GetGraphic();
if (graphic is CIMLineGraphic lineGraphic) {
//change its symbol
lineGraphic.Symbol =
SymbolFactory.Instance.ConstructLineSymbol(
SymbolFactory.Instance.ConstructStroke(
ColorFactory.Instance.BlueRGB, 2, SimpleLineStyle.DashDot)).MakeSymbolReference();
//apply the change
ge.SetGraphic(lineGraphic);
}
Note: The GraphicElement.Graphic
property is deprecated at 2.6. Use GetGraphic() instead.
When a graphics element or group element is selected, it means its bounding box is displayed on the view and grab handles are also shown. Selected elements can be moved and resized interactively with any of the COTS graphics tools.
Setting elements as selected can be done programmatically or graphically on the view. Programmatic selection takes the element or elements to be selected and highlights them on the screen. Use SelectElement(element) and SelectElements(list_of_elements) accordingly. Graphic selection uses the selection geometry to intersect the geometries of those elements (graphic or group) that will be selected and then highlights them. In this example, all the elements with a name beginning with "Text" are selected programmatically:
//on the QueuedTask
var elements = graphicsLayer.GetElementsAsFlattenedList()
.Where(e => e.Name.StartsWith("Text"));
graphicsLayer.SelectElements(elements);
In this example a sketch geometry is being used to do a graphic element selection on the view:
//on the QueuedTask...note: the selected elements may belong to more than one
//layer...
var elems = MapView.Active.SelectElements(selPoly, SelectionCombinationMethod.New);
When selecting elements programmatically, only a group element or its children can be selected but not both. This is reflected on the display when selected elements are highlighted. Note that the child elements nested within a given group element are never highlighted in conjunction with their parent - it's an either/or. If you specify a group element and one or more of its children in the list of elements to be selected then the child elements are filtered out of the list. When selecting graphically, the view element selection automatically applies the same filtering logic (the same is true for element selection on a layout) so that if a group element and one or more of its children are intersected by the selection geometry, the parent group element will be element that is selected.
Group can be selected...or | Its children can be selected....but not both |
---|---|
In this example, a group element and its children are being specified for selection. The selection logic will filter out the children selecting only the (top most) parent - the group element in this case.
//on the QueuedTask
//get a group element
var group = graphicsLayer.GetElements().OfType<GroupElement>().First();
//try to select the group element and its children
var elements = new List<Element>() { group };
elements.AddRange(group.Elements);
//children are filtered out...
graphicsLayer.SelectElements(elements);//only the group element will be selected...
Unselecting elements is the same as programmatic selection only in reverse. The list of elements provided as the argument to UnSelectElements are unselected (or UnSelectElement if a single element is unselected). Their highlight on the view is cleared. Specifying an empty or null list of elements as the parameter results in all elements for the graphics layer being unselected. Specify a null element as the element to be unselected via UnSelectElement (singular) will throw a ArgumentNullException
.
//On the QueuedTask...
//unselect the first element in the currently selected elements
var elem = graphicsLayer.GetSelectedElements().FirstOrDefault();
if (elem != null)
graphicsLayer.UnSelectElement(elem);//will throw if 'elem' is null
//unselect all elements
graphicsLayer.UnSelectElements();//equivalent to graphicsLayer.ClearSelection()
Whenever the selection on a graphics layer changes (elements are selected or unselected), the ArcGIS.Desktop.Mapping.Events.ElementSelectionChangedEvent is fired. The event args contain the container whose selection changed and the count of elements selected. If the selection change was an unselect or clear select, the count of selected elements will be zero. The event can fire more than once per selection. For example, assume a map contains two graphics layers with some elements already selected on one of the layers. Another selection is made (eg with the COTS select elements tool) in the layer with no elements selected. The ElementSelectionChangedEvent fires twice. Once for the graphics layer whose selection changed because elements were selected (via the tool) and once for the graphics layer whose selection got cleared because no elements were selected (by the tool). Subscribing to the event is shown below:
//subscribe to the ElementSelectionChangedEvent event
ArcGIS.Desktop.Mapping.Events.ElementSelectionChangedEvent.Subscribe((args) => {
//check the container is a graphics layer - could be a Layout (or even map view)
if (args.ElementContainer is GraphicsLayer graphicsLayer) {
//get the total selection count for the container
var count = args.SelectedElementCount;
//Check count - could have been an unselect or clearselect
if (count > 0) {
//this is a selection or add to selection
var elems = graphicsLayer.GetSelectedElements();
//TODO process the selection...
}
else {
//This is an unselect or clear select
//TODO process the unselect or clear select
}
}
});
To add or remove elements to a group use the GroupElements and UnGroupElement or UnGroupElements methods respectively.
When elements are grouped (to include other group elements) a new group element is created that contains the "grouped" elements. The elements' parent is set to the newly created Group Element. Elements can be grouped in any number of nested hierarchies (groups contained within groups contained within groups ....) as required. Ungrouping elements ungroups the elements within the specified group element hierarchy. Elements should be ungrouped from the top down not the bottom up. To ungroup multiple hierarchies, UnGroupElement(s) should be called multiple times starting with the top most group element in the hierarchy.
The bounding box of a group element will be set to the union of the bounding boxes of all its contained elements. Child elements are accessed via the group element Elements property.
Accessing elements contained within a nested hierarchy requires recursing the Elements
collection of each Group Element in a given element's parent hierarchy. Note: the top most parent of all elements is always its container - either a layout of GraphicsLayer.
//on the QueuedTask
//group some elements
var group_elem = graphicsLayer.GroupElements(graphicsLayer.GetSelectedElements());
//flattening the element collection of a GroupElement
var flat_list = new List<Element>();
Flatten(group_elem.Elements, flat_list);
...
//check the parent
var parent = group_elem.Elements.First().GetParent();//will be the group element
//top-most parent
var top_most = group_elem.Elements.First().GetParent(true);//will be the GraphicsLayer
//flatten the list of elements
private static void Flatten(IEnumerable<Element> elements, IList<Element> flattened) {
foreach (var element in elements)
{
flattened.Add(element);
// Tail recursion for Group Elements.
if (element is GroupElement)
Flatten((element as GroupElement).Elements, flattened);
}
}
//Ungroup grouped elements
var selectedElements = graphicsLayer.GetSelectedElements().ToList(); ;
var elementsToUnGroup = new List<GroupElement>();
//All selected elements should be grouped.
if (selectedElements.Count() == selectedElements.OfType<GroupElement>().Count()) {
//Convert to a GroupElement list.
elementsToUnGroup = selectedElements.ConvertAll(x => (GroupElement)x);
}
//UnGroup
graphicsLayer.UnGroupElements(elementsToUnGroup);
Use RemoveElement or RemoveElements specifying the element or elements to be removed. Removing a Group Element will remove all of its children. Specifying null or an empty list as the list of elements to be removed results in a no-op.
//on the QueuedTask
//remove all the currently selected elements
graphicsLayer.RemoveElements(graphicsLayer.GetSelectedElements());
Elements from one graphics layer can be copied to any other graphics layer in the map. Copying from multiple layers requires multiple copy operations - one per layer. Elements can be copied into the graphics layer at the root level or into a group element within the graphics layer.
//on the QueuedTask
var elems = sourceLayer.FindElements(new List<string>() { "Point 1", "Line 3", "Text 1"});
var copiedElements = targetLayer.CopyElements(elems);
Z-Order controls the order of display of individual graphics within their respective containers. Z-order runs from 0 to n-1 where 0 is the bottom of the z-order and this element draws first, and n-1 is the top of the z-order and this element draws last. Z-order of a given element can only be changed within that element's respective parent container. For example, if an element is a child of a group element, then it can be moved up or down in the z-order of children in that group element. The group element can be moved up or down in the z-order of children within its parent and so-on. This is not that much different than the way z-order works for layers in the map TOC or for elements in the Layout TOC. Layers at the bottom of the TOC (low z-order) draw first, layers at the top of the TOC (high z-order) draw last. Ditto for elements in the layout TOC.
To change the z-order of an element or elements use BringForward and SendBackward. To move an element or elements to the top or bottom of their z-order use BringToFront and SendToBack respectively. If more than one element is specified then all elements must have the same parent (eg be children of the same group element or all be immediate children of the graphics layer). If elements do not have the same parent then an ArgumentException
will be thrown. To check whether an element or elements' z-order can be changed call CanBringForward and CanSendBackward.
//On the QueuedTask
//get the current selection set
var sel_elems = graphicsLayer.GetSelectedElements();
//can they be brought forward? This will also check that all elements have the same parent
if (graphicsLayer.CanBringForward(sel_elems)) {
//bring forward
graphicsLayer.BringForward(sel_elems);
//bring to front (of parent)
//graphicsLayer.BringToFront(sel_elems);
}
else if (graphicsLayer.CanSendBackward(sel_elems)) {
//send back
graphicsLayer.SendBackward(sel_elems);
//send to the back (of parent)
//graphicsLayer.SendToBack(sel_elems);
}
//list out the z order
foreach(var elem in sel_elems)
System.Diagnostics.Debug.WriteLine($"{elem.Name}: z-order {elem.GetZOrder()}");
Elements can be moved programmatically by applying a new anchor point via element.SetAnchorPoint(). The element will automatically move its location such that its anchor point is re-positioned at the new position specified. The relative position of the anchor point does not change (to change the anchor use element.SetAnchor(anchor) where anchor can be one of 9 relative positions)
Elements can also be moved interactively once they are selected by using the built-in element selection tool (which changes to "move" mode whenever the cursor is over or "hittests" against a selected element).
In this example, a user is using a custom map tool to move the selected elements to a point he or she clicks on the map. The tool calculates the distance between the point clicked and the closest element in the selected set then moves all elements "that" distance. This maintains the relative position of all the selected elements to each other (rather than them all simply being moved, one on top of the other, to the same point):
//in the tool ctor
public MoveGraphicsTool() {
SketchType = SketchGeometryType.Point;
...
protected override Task<bool> OnSketchCompleteAsync(Geometry geometry) {
var elems = new Dictionary<Element, Coordinate2D>();
var pt = geometry as MapPoint;//the point clicked
var sr = MapView.Active.Map.SpatialReference;
var graphicsLayers = MapView.Active.Map.GetLayersAsFlattenedList()
.OfType<GraphicsLayer>();
return QueuedTask.Run(() => {
var coords = new List<Coordinate2D>();
foreach(var graphicsLayer in graphicsLayers) {
foreach(var elem in graphicsLayer.GetSelectedElements()) {
//check minimum distance to the clicked point
var anchor = elem.GetAnchorPoint();
coords.Add(anchor);
elems.Add(elem, anchor);
}
}
var mb = new MultipointBuilderEx(coords, sr);
var nearest_res = GeometryEngine.Instance.NearestPoint(mb.ToGeometry(), pt);
//move all elements the same minimum distance to maintain their position
//relative to each other
var dx = pt.X - nearest_res.Point.X;
var dy = pt.Y - nearest_res.Point.Y;
//do the move
foreach(var kvp in elems) {
var anchor = new Coordinate2D(kvp.Value.X + dx, kvp.Value.Y + dy);
kvp.Key.SetAnchorPoint(anchor);
}
return true;
});
}