Nested group layers with ArcPy in ArcGIS Pro

1002
3
07-26-2023 05:57 AM
lbd_bayern
New Contributor II

Hello everyone,

I am trying to create a multi-level layer hierarchy of group layers with ArcPy.

Consider the following example:

example.png

At the top of the hierarchy, let's say I have a group layer named level_1_group_layer. This group layer has two group layers as children, level_2_group_layer_a and level_2_group_layer_b. Each of these two group layers has two "normal" layers as children (level_3_layer_a and level_3_layer_b as children of level_2_group_layer_alevel_3_layer_c and level_3_layer_d as children of level_2_group_layer_b).

Is there a way to achieve this nested hierarchy of group layers via ArcPy?

I am able to place "normal" layers (e.g. level_3_layer_a and level_3_layer_b) below a group layer (e.g. level_2_group_layer_a). For this, I am using the method addLayerToGroup(group, layer) of the activeMap object as described here: Solved: Is it possible to create a new group layer with ar... - Esri Community

However, so far, I was not successful in adding a group layer (e.g. level_2_group_layer_a) as a child of another group layer (e.g. level_1_group_layer).

  • Trying to use the method addLayerToGroup(level_1_group_layer, level_2_group_layer_a) results in an exception: Traceback (most recent call last):
    File "<string>", line 114, in <module>
    File "<string>", line 53, in remove_layer_from_map__and__place_layer_below_group_layer
    File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\utils.py", line 191, in fn_
    return fn(*args, **kw)
    File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\_mp.py", line 2593, in addLayerToGroup
    return convertArcObjectToPythonObject(self._arc_object.addLayerToGroup(*gp_fixargs((target_group_layer, add_layer_or_layerfile, add_position), True)))
    ValueError: <MappingLayerObject object at 0x000001701373CED0>
  • For activeMap, I found the method moveLayer(reference_layer, move_layer, {insert_position}) (Map—ArcGIS Pro | Documentation) which from the description seems to be exactly what I need (with e.g. level_1_group_layer as reference_layer and level_2_group_layer_a as move_layer). However, for the parameter insert_position, only the values "AFTER" and "BEFORE" are available. For "AFTER", it is stated that  it "inserts the new layer after or below the reference layer". When I now call active_map.move_layer(level_1_group_layer, level_2_group_layer_a, "AFTER"), level_2_group_layer_a is inserted after level_1_group_layer, i.e. on the same hierarchy level. Is there any way to a ensure it is inserted below , i.e. that level_2_group_layer_a will be one level below level_1_group_layer in the layer hierarchy?

Am I using the methods incorrectly? Or are there alternative ways to achieve the nested hierarchy of group layers? Any help is very much appreciated!

0 Kudos
3 Replies
JohannesLindner
MVP Frequent Contributor

So, this was an adventure...

You can do it, but it's complicated and there are several pitfalls along the way.

  • You can't add an empty group layer file to an existing group layer (as you found out)
  • You can only add layers to a group layer, so you have to convert your input feature classes first
  • You could do it with MakeFeatureLayer, but there was some problem ( I forgot what it was exactly)
  • So the best way I found is to use map.addDataFromPath, add the resulting layer to a group with map.addLayerToGroup and then delete the original layer with map.removeLayer
  • You need to traverse your layer list recursively to be able to add group layers to group layers
  • Bonus: You can't use the same lyrx when you are in recursive mode. When you add an empty group lyrx to the map and then try to add the same lyrx to it, it will raise a RuntimeError and complain about missing credentials.
  • So you actually need to create new empty layer files as you go, so you need to find a way to do that (fortunately, it is quite easy)
  • And then you realize that it always adds layers to the top of the toc, so you need to find a way to combat that. addDataToPath doesn't have a position argument, so you would have to do it with map.moveLayer, but I did not want to go down that rabbit hole, so I just used reversed() in 2 places to traverse the layer structure from end to start. This works for feature classes with the same geometry type, but arcpy changes position according to geometry type (points above lines above polygons), so it might not work for different geometry types.

 

With all that said, here is the very ugly result:

from pathlib import Path
from uuid import uuid4


def create_group_layer_file(folder, name):
    """Creates an empty group layer file.

    folder: str or pathlib.Path, the folder where the file will be saved
    name: str, the name of the layer (without extension)

    Returns a str of the layer file's path

    """
    uRI = f"CIMPATH={uuid4()}.json"  # unique id for the layer
    cim = {
        "type" : "CIMLayerDocument", # this is a layer document
        "layers" : [                 # these are the included layers
            uRI
            ],
        "layerDefinitions" : [       # and these are the layers' definitions
            {
              "type" : "CIMGroupLayer",
              "name" : name,
              "uRI" : uRI
            }
          ]
        }
    # create the file
    lyrx = Path(folder) / f"{name}.lyrx"
    with lyrx.open("w") as f:
        f.write(str(cim))
    # return the path as str
    return str(lyrx)


def add_layer_recursive(target_map, layer, folder, group=None):
    """Adds a layer to a map. If the specified layer is a group layer, it adds
    all its children recursively.

    target_map: arcpy.mp.Map, the map to which the layer will be added
    layer: str or dict, the layer that will be added
        str: full path to a feature class, this will add a feature layer
        dict: dictionary with the keys "name" and "layers", this will add
            a group layer with the specified name and then it will add all
            specified layers to this group
    folder: str or pathlib.Path, a folder where temporary layer files will be created
        you need write access to that folder!
    group: arcpy.mp.Layer, the group layer to which teh layer will be added
        only important for recursion, call the function with group=None

    """
    # layer is path to feature class
    if isinstance(layer, (str, Path)):
        print(f"Adding {layer} to the map")
        lyr = target_map.addDataFromPath(str(layer))
        if group is not None:
            print(f"\tMoving {layer} to group layer {group.name}")
            target_map.addLayerToGroup(group, lyr)
            target_map.removeLayer(lyr)
    # layer is group layer definition dictionary
    elif isinstance(layer, dict):
        print(f"Adding group layer {layer['name']} to the map")
        new_group_lyrx = create_group_layer_file(folder, layer["name"]) # create lyrx
        new_group = target_map.addDataFromPath(new_group_lyrx)
        new_group.name = layer["name"]
        Path(new_group_lyrx).unlink() # delete lyrx
        # call this function recursively
        for sub_layer in reversed(layer["layers"]):
            add_layer_recursive(target_map, sub_layer, temp_dir, new_group)
        # and move the subgroup
        if group is not None:
            print(f"\tMoving group layer {new_group.name} to group layer {group.name}")
            target_map.addLayerToGroup(group, new_group)
            target_map.removeLayer(new_group)




# the map to which the layers will be added
target_map = arcpy.mp.ArcGISProject("current").activeMap
# a folder where you have write access
temp_dir = r"G:\ArcGIS\data\temp"
# the layer structure
    # feature layers as path to their source (str or pathlib.Path)
    # group layers as dict with the keys "name" and "layers"
gdb = Path(arcpy.env.workspace)
layers = [
    gdb/"FC0",
    {"name": "GroupA", "layers":[
        gdb/"FC1",
        gdb/"FC2",
        ]},
    gdb/"FC3",
    {"name": "GroupB", "layers":[
        {"name": "GroupBA", "layers": [
            gdb/"FC4",
            gdb/"FC5",
            ]},
        {"name": "GroupBB", "layers": [
            gdb/"FC6",
            {"name": "GroupBBA", "layers": [
                gdb/"FC7",
                gdb/"FC8",
                ]},
            ]},
        ]},
    gdb/"FC9",
    ]


# create the temp dir
Path(temp_dir).mkdir(exist_ok=True)
# add the layers
for layer in reversed(layers):
    add_layer_recursive(target_map, layer, temp_dir, None)

 

Because the function works recursively, you can do as many levels as you like. Maybe ArcGIS has some limit, but the function doesn't.

For example, the layer structure in lines 86 and onwards nests 3 group layers:

JohannesLindner_0-1690497746337.png

 


Have a great day!
Johannes
0 Kudos
lbd_bayern
New Contributor II

So I finally found time testing this.

I am a bit confused because the following happened:

Before, I was able to move Feature Layers below a Group Layer, essentially using this code:

 

def move_feature_layer_below_group_layer(feature_layer, group_layer, target_map):
        target_map.removeLayer(feature_layer)
        target_map.addLayerToGroup(group_layer, feature_layer)

 

When trying to move Group Layers below a Group Layer, the code failed (as described in my original post).

Now - for whatever reason - it is exactly the other way round: Using the two methods above, I can move Group Layers below a Group Layer, but trying to move a Feature Layer below a Group Layer fails. 

For the first case, when moving Group Layers below a Group Layer, it also works as intended if these moved Group Layers are parents of Feature Layers. So this is great for my use case. See the following example, displaying the initial state on the left and the result after running my script on the right (I manually arranged the initial state on the left in ArcGIS Pro):

GroupLayers_below_GroupLayer.png

For the second case (moving Feature Layers below a Group Layer), my once working script now produces the following error:

Traceback (most recent call last):
File "<string>", line 201, in <module>
File "<string>", line 111, in hf_remove_layer_from_map__and__readd_layer_below_group_layer
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\utils.py", line 191, in fn_
return fn(*args, **kw)
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\_mp.py", line 2593, in addLayerToGroup
return convertArcObjectToPythonObject(self._arc_object.addLayerToGroup(*gp_fixargs((target_group_layer, add_layer_or_layerfile, add_position), True)))
ValueError: <MappingLayerObject object at 0x00000162667A60C0>

Have there been any changes to the API? 

I am very confused because according to the install info

 

arcpy.GetInstallInfo()

 

, my installation should still be the same as when I made my original post:

{'LicenseLevel': 'Basic', 'InstallDir': 'c:\\program files\\arcgis\\pro\\', 'ProductName': 'ArcGISPro', 'Version': '3.1.2', 'SourceDir': '1', 'InstallType': 'N/A', 'BuildNumber': '41833', 'InstallDate': '30.06.2023', 'InstallTime': '12:21:38'}

Any ideas what is causing the problems, or suggestions how I can now move Feature Layers below a Group Layer?

For moving Feature Layers, I also tried the trick with the lyrx file from Johannes. Here my hard-coded, simple version (no need for recursion here):

 

def move_feature_layer_below_group_layer_using_lyrx(feature_layer, group_layer, target_map):
    new_feature_lyrx = hf__create_feature_layer_file(r"C:\MyPrograms\temp\tmp_files", feature_layer.name)
    new_feature = target_map.addDataFromPath(new_feature_lyrx)
    new_feature.name = feature_layer.name
    target_map.addLayerToGroup(group_layer, new_feature)
    target_map.removeLayer(new_feature)
    Path(new_feature_lyrx).unlink()

 

Using the following version of the subroutine hf__create_feature_layer_file, I am getting the same error as above (red text):

 

def hf__create_feature_layer_file(folder, name):
    uRI = f"CIMPATH={uuid4()}.json"  # unique id for the layer
    cim = {
    "type" : "CIMLayerDocument", # this is a layer document
    "layers" : [                 # these are the included layers
        uRI
        ],
    "layerDefinitions" : [       # and these are the layers' definitions
        {
          "type" : "CIMFeatureLayer",
          "name" : name,
          "uRI" : uRI
        }
      ]
    }
    # create the file
    lyrx = Path(folder) / f"{name}.lyrx"
    with lyrx.open("w") as f:
        f.write(str(cim))
    # return the path as str
    return str(lyrx)

 

Traceback (most recent call last):
File "<string>", line 236, in <module>
File "<string>", line 171, in hf_remove_layer_from_map__and__readd_layer_below_group_layer2
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\utils.py", line 191, in fn_
return fn(*args, **kw)
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\_mp.py", line 2593, in addLayerToGroup
return convertArcObjectToPythonObject(self._arc_object.addLayerToGroup(*gp_fixargs((target_group_layer, add_layer_or_layerfile, add_position), True)))
ValueError: <MappingLayerObject object at 0x000001629A97AE40>

Using a slightly adapted version of hf__create_feature_layer_file, I am getting a different error:

 

def hf__create_feature_layer_file(folder, name, feature_layer):
    uRI = f"CIMPATH={uuid4()}.json"  # unique id for the layer
    cim = {
    "type" : "CIMLayerDocument", # this is a layer document
    "layers" : [                 # these are the included layers
        feature_layer
        ],
    "layerDefinitions" : [       # and these are the layers' definitions
        {
          "type" : "CIMFeatureLayer",
          "name" : name,
          "uRI" : uRI
        }
      ]
    }
    # create the file
    lyrx = Path(folder) / f"{name}.lyrx"
    with lyrx.open("w") as f:
        f.write(str(cim))
    # return the path as str
    return str(lyrx)

 

Traceback (most recent call last):
File "<string>", line 236, in <module>
File "<string>", line 169, in hf_remove_layer_from_map__and__readd_layer_below_group_layer2
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\utils.py", line 191, in fn_
return fn(*args, **kw)
File "C:\Program Files\ArcGIS\Pro\Resources\ArcPy\arcpy\_mp.py", line 2764, in addDataFromPath
return convertArcObjectToPythonObject(self._arc_object.addDataFromPath(*gp_fixargs((data_path, web_service_type, custom_parameters,), True)))
RuntimeError: Daten konnten nicht hinzugefügt werden. Mögliches Problem bei den Anmeldeinformationen.

(The German sentence at the end translates to "Data could not be added. Possible problem with the credentials.")

This error indicates a problem with the writing permissions. I am not sure why this occurs, though, as my user definitely has write permissions for the specified folder.

I am running my scripts in the ArcGIS Pro Python window, if this would make any difference for the described problems.

Sorry for the wall of text. 

0 Kudos
lbd_bayern
New Contributor II

Hi Johannes,

wow, this is really much more complicated than I would have hoped for.

Thank your very, very much for looking into this and all the effort you invested. I will try to incooperate your solution into my scripts once I am finding the time for it (right now, there is also a lot of other work to do, unfortunately ). In any case, I will come back here and provide some feedback on how it worked out for me.

Again, thank you so much for now! 😺

0 Kudos