I am struggling to apply symbology from an existing layer file to an output from a script tool. The below snippet is from a larger script tool that takes a DEM, clips it to the Map View, smooths it using Focal Statistics, creates a feature class of contours in an existing geodatabase, and then adds an index field. The contour feature class is automatically added to the map (seemingly regardless of my use of env.addOutputsToMap). Seems like the use of either "Add Field" or "Calculate Field" does this, which is fine, I want it there anyway.
out_gdb = r"F:\ContourTest\ContourTest.gdb"
cont_file = fr"{out_gdb}\Contours_20"
current_project = arcpy.mp.ArcGISProject("CURRENT")
current_map = current_project.listMaps()[0]
current_map.addDataFromPath(fr"{cont_file}")
cont_layer = current_map.listLayers("Contour*")[0]
symbology = r"F:\ContourTest\Contour_Index.lyrx"
arcpy.management.ApplySymbologyFromLayer(in_layer=cont_layer,
in_symbology_layer=symbology,
symbology_fields="VALUE_FIELD Index_Contour Index_Contour",
update_symbology="MAINTAIN")
The above code is the last bit of the larger script. The whole thing, including this bit, runs without exceptions, but the symbology does not apply as expected.
But, if I then take this bit of code and paste it exactly into the Python Window and run it, another copy of the contour feature class is added to the TOC, and the correct symbology is applied. Works perfectly. If I then remove both, the symbolized contours and the unsymbolized contours, and run the code above in the Python Window again, just a symbolized version is added (or an unsymbolized version is added, and then the symbology is applied, either way). Again, works perfectly.
I have tried using versions of MakeFeatureLayer, addLayer, all kinds of things--I am likely getting tripped up over the colloquial use of the term "layer," vs. the technical meaning of layer, layer file, layer object in code vs path to layer, etc. etc.
Happy to paste the entire script if that is helpful, just didn't want to clutter the question. This a script tool in a regular toolbox (.tbx) with parameters and some basic validation set up within Pro. The validation pertains to whether a contour index field should be created, checking for integer inputs, etc., so I don't think it should interfere here, but who knows. I am running ArcGIS Pro 2.5.0--it tells me that this is the current version.
Solved! Go to Solution.
I know this is a little old, but I haven't found a solution mentioned online. It's quite an easy solution, but it is not well documented, if it is at all. I had to piece this together from reading different things. It actually does not even involve using the ApplySymbologyFromLayer method.
You can use the options given in @DrewFlater response and also use an output name generated in the script. The solution:
# APPLIED AFTER MAKE FEATURE LAYER
fl = arcpy.management.MakeFeatureLayer(parcels,prop_name,tmap_exp)
# using SetParameterAsText... first argument is index of your script tool's parameters, and second arg is the feature layer I generated.
arcpy.SetParameterAsText(6,fl)
# APPLIED TO A SHAPEFILE
arcpy.env.workspace = filepath
formatted_name = prop_name.replace(" ","_")
shp = arcpy.management.CopyFeatures(fl,"{}.shp".format(formatted_name))
arcpy.SetParameterAsText(6,shp)
I haven't spent a lot of time working with python toolboxes in my career (no real need for them), but recently I've had to adapt what was meant to be a standalone script into a toolbox, after discovering a bug that affects CIM (v2) FeatureLayer objects that are accessed outside of an ArcGIS Pro application.
My tool is designed to be run on an .aprx that contains small scale transportation maps (and associated layouts) with inset areas marked on them, alongside separate large scale maps (and associated layouts) covering the inset areas. The idea is to take each polyline layer in the small scale map and break it into two layers. Layer one retains the existing symbology, but is masked inside the bounds of the inset areas. Layer two is a copy of layer one with a definition query applied (limiting it to features that intersect the inset area), no layer mask, and reduced line weights for less visual clutter.
My workflow in the standalone script went something like this:
#1 Make the new layer, add it to the map, and move it to the correct location
result = arcpy.management.MakeFeatureLayer(old_layer, new_name)
pro_map.addLayer(result.getOutput(0))
new_inset_layer = pro_map.listLayers(new_name)[0]
pro_map.moveLayer(old_layer, new_inset_layer, "AFTER")
#2 Apply the Unique Values renderer to the new layer
arcpy.management.ApplySymbologyFromLayer(new_inset_layer, old_layer)
At this point, we need to bring in the CIM. ApplySymbologyFromLayer sets the new layer's renderer without issue, but it defaults to using the literal values from the attribute fields as labels for the symbol classes. That's no good; this is a Unique Value renderer with multiple fields and grouped values. We don't want a label that reads:
Unpaved Road, SR; Unpaved Road, 4; Unpaved Road, 5; Unpaved Road, NA
we just want it to say Unpaved Road. But we can pull those settings over from the old layer, too, using the CIM:
#2A Copy labels
inset_cim = new_inset_layer.getDefinition('V2')
inset_cim.renderer.groups[0].classes = old_layer.getDefinition('V2').renderer.groups[0].classes
From there, it's a relatively straightforward process of looking up the different symbol classes in a symbol dictionary that specifies the updated line weights and colors. (I could see how setting up a .lyrx template and retrieving the desired symbology from there might be easier than using a dictionary, but I was having fun learning the ins and outs of the CIM)
So I've got a solution, but now I'm wondering: how does the derived output parameter(s?) even begin to fit into this picture? As already noted, there isn't a ton of documentation out there (maybe even less than there is for the CIM!) and this thread is one of the few links that come up when I search for help.
(P.S. I would agree that this issue has not really been 'solved' in a meaningful way. I have no problem with Esri saying "We don't consider this a bug and we don't plan to fix it," but then they should at least provide with some clear, detailed examples of how the derived parameters workaround is intended to work.)
Hey there! I recently faced a similar problem. ApplySymbologyFromLayer function was not working in python notepad. I'm not sure if the bug hasn't been resolved by ESRI yet or if it's a problem with my software version (3.1). Ultimately, I managed to work around the problem in the following way:
1. I created a style (my_style.stylx file), where I wrote down what the feature should looklike depending on the attribute value - the attribute value is the name of the style icon.
2. Since my attributes were numeric, and in order to match them to the syle they must be strings, I wrote a loop in Python to give the new column (new_column) a string value depending on the numerical values.
3. I used the function arcpy.management.MatchLayerSymbologyToAStyle(my_layer, f"$feature.new_column", r"path_to_style\my_style.stylx")
The function "MatchLayerSymbologyToAStyle) works in python just fine.
@JeffBarrette @JeffMoulds any thoughts on this thread? ApplySymbologyFromLayer is extremely buggy. I am able to get it to work in one map frame but not in a second non-active map frame within my python script. Could this be a case where the map has to be activated for the script to be able to access the layers within that map?
Thanks @MK13 for bringing this to my attention. This is something we are looking into and need to coordinate with the core GP/arcpy team. Here is a brief explanation and a couple of possible work-arounds.
ApplySymbologyFromLayer is a core GP/arcpy method. It runs on the GP thread which includes includes GP dialogs, models, and script tools. The GP thread provides isolation so the application (main thread) can continue to run. Arcpy.mp was designed to work with the application objects and also runs on the main thread. When using a script tool, its important to hook the results back to the main thread and this can be done using the technique mentioned by @TanGnar above.
import os
p = arcpy.mp.ArcGISProject('current')
relpath = p.homeFolder
lyr = arcpy.GetParameter(0)
if lyr.name == ‘GreatLakes’:
lyrx = os.path.join(relpath, 'GreatLakes.lyrx’)
arcpy.management.ApplySymbologyFromLayer(lyr, lyrx)
arcpy.SetParameter(1, lyr)
Another alternative is to use an all arcpy.mp solution where you "copy" the symbology using this technique.
import os
p = arcpy.mp.ArcGISProject('current')
relpath = p.homeFolder
for lyr in p.listMaps(‘Map’).listLayers():
if lyr.name == ‘GreatLakes’:
lyrx = os.path.join(relpath, 'GreatLakes.lyrx’)
lyrx_lyr = lyrx.listLayers(‘GreatLakes’)[0]
lyr.symbology = lyrx_lyr.symbology
And at Pro 3.4 we hope to introduce a new (feature) Layer.pasteProperties(source_layer, properties) method that allows you copy existing layer properties from a source layer where it defaults to all default properties OR you can specify keywords such as symbology, or definition queries, field aliases, or pop-ups, etc,
Jeff - Layout and arcpy.mp teams
@JeffBarrette updating the symbology using lyr.symbology = lyrx_lyr.symbology as in your code above brings in the symbols but doesn't bring in the label classes from the original layer file. I have 2 label classes defined in the layer file but I see only one label class in the layer whose symbology has been updated and the label class' symbology was not maintained as it was in the layer file. Is that a setting that has to be set?
@MK13 Again, this is just a work around but have you tried something like ...
import os
p = arcpy.mp.ArcGISProject('current')
relpath = p.homeFolder
m = p.listMaps('Map')[0]
for l in m.listLayers():
if l.name == 'GreatLakes':
lyr1 = arcpy.mp.LayerFile(os.path.join(relpath, 'GreatLakes.lyrx')).listLayers('GreatLakes')[0]
l.symbology = lyr1.symbology#Also copy label classes
l_cim = l.getDefinition('V3')
lyr1_cim = lyr1.getDefinition('V3')
l_cim.labelClasses = lyr1_cim.labelClasses
l.setDefinition(l_cim)
l.showLabels = True #Just in case they are not visible
Jeff