Hello everyone,
I am trying to create a multi-level layer hierarchy of group layers with ArcPy.
Consider the following example:
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_a, level_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).
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!
So, this was an adventure...
You can do it, but it's complicated and there are several pitfalls along the way.
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:
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):
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.
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! 😺