# -*- coding: utf-8 -*- import arcpy # type: ignore DEFAULT_LINE_WIDTH = 0.7 class Toolbox: def __init__(self): self.label = "Feature Renderer" self.alias = "Feature Renderer" # List of tool classes associated with this toolbox self.tools = [FeatureRenderer] class FeatureRenderer: def __init__(self): self.label = "Feature Renderer" self.description = "Unique value renderer for geologic features" def getParameterInfo(self): # Define parameter for selecting style files style_files = arcpy.Parameter( displayName="Select Style Files", name="style_files", datatype="DEFile", parameterType="Required", direction="Input", multiValue=True, ) # Set filter to only show .stylx files in the file picker style_files.filter.list = ["stylx"] # Define parameter for selecting feature classes or layers layers = arcpy.Parameter( displayName="Select Feature Classes or Layers", name="fc_or_layer", datatype="GPFeatureLayer", parameterType="Required", direction="Input", multiValue=True, ) # Define parameter for selecting a table from the enterprise geodatabase table_param = arcpy.Parameter( displayName="Select Legend Table", name="table", datatype="GPTableView", parameterType="Required", direction="Input", ) # Define parameter for selecting a primary key field_param_1 = arcpy.Parameter( displayName="Select Primary Key Field (LEG_ID)", name="primary_key", datatype="Field", parameterType="Required", direction="Input", multiValue=False, ) field_param_1.parameterDependencies = [table_param.name] # Define parameter for selecting a fill symbol code field field_param_2 = arcpy.Parameter( displayName="Select Fill Symbol Code Field", name="fill_symbol", datatype="Field", parameterType="Required", direction="Input", multiValue=False, ) field_param_2.parameterDependencies = [table_param.name] # Define parameter for selecting a line symbol code field field_param_3 = arcpy.Parameter( displayName="Select Line Symbol Code Field", name="line_symbol", datatype="Field", parameterType="Required", direction="Input", multiValue=False, ) field_param_3.parameterDependencies = [table_param.name] # Define parameter for selecting a marker symbol code field field_param_4 = arcpy.Parameter( displayName="Select Marker Symbol Code Field", name="point_symbol", datatype="Field", parameterType="Required", direction="Input", multiValue=False, ) field_param_4.parameterDependencies = [table_param.name] # Define parameter for selecting a legend text field field_param_5 = arcpy.Parameter( displayName="Select Legend Text Field 1", name="legend_text_1", datatype="Field", parameterType="Optional", direction="Input", multiValue=False, ) field_param_5.parameterDependencies = [table_param.name] # Define parameter for selecting a legend text field field_param_6 = arcpy.Parameter( displayName="Select Legend Text Field 2", name="legend_text_2", datatype="Field", parameterType="Optional", direction="Input", multiValue=False, ) field_param_6.parameterDependencies = [table_param.name] # Define parameter for label field label_field_1 = arcpy.Parameter( name="label_field_1", displayName="Select Label Field 1", datatype="Field", direction="Input", parameterType="Optional", category="Define Label", ) # Define parameter for delimiter label_delimiter = arcpy.Parameter( name="label_delimiter", displayName="Delimiter", datatype="GPString", direction="Input", parameterType="Optional", category="Define Label", ) label_delimiter.value = "-" # Define parameter for label field label_field_2 = arcpy.Parameter( name="label_field_2", displayName="Select Label Field 2", datatype="Field", direction="Input", parameterType="Optional", category="Define Label", ) label_field_1.parameterDependencies = [table_param.name] label_field_2.parameterDependencies = [table_param.name] # Define parameter for heading field heading_field = arcpy.Parameter( name="heading_field", displayName="Select Heading Field", datatype="Field", direction="Input", parameterType="Optional", category="Define Heading", ) heading_field.parameterDependencies = [table_param.name] # Define parameter to choose to draw outlines draw_outlines = arcpy.Parameter( displayName="Draw outlines", name="draw_outlines", datatype="GPBoolean", parameterType="Required", direction="Input", category="Define Outlines", ) # Default value draw_outlines.value = True # Define parameter for outline field outline_field = arcpy.Parameter( name="outline_field", displayName="Select Outline Field", datatype="Field", direction="Input", parameterType="Optional", category="Define Outlines", ) outline_field.parameterDependencies = [table_param.name] # Return parameter definitions as a list return [ style_files, layers, table_param, field_param_1, field_param_2, field_param_3, field_param_4, label_field_1, label_delimiter, label_field_2, heading_field, draw_outlines, outline_field, ] def isLicensed(self): return True def updateParameters(self, parameters): return def updateMessages(self, _): return def execute(self, parameters, messages): # Retrieve a reference to the current project project = arcpy.mp.ArcGISProject("CURRENT") # Retrieve input parameters style_files = parameters[0].valueAsText input_layers = parameters[1].valueAsText table = parameters[2].valueAsText primary_key_field = parameters[3].valueAsText fill_symbol_code_field = parameters[4].valueAsText line_symbol_code_field = parameters[5].valueAsText point_symbol_code_field = parameters[6].valueAsText label_field_1 = parameters[7].valueAsText label_delimieter = parameters[8].valueAsText label_field_2 = parameters[9].valueAsText heading_field = parameters[10].valueAsText draw_outlines = parameters[11].value outline_field = parameters[12].valueAsText # Retrieve currently active map active_map = project.activeMap # Retrieve project layers map_layers = [] if active_map: map_layers = active_map.listLayers() else: raise ValueError("Execution aborted: please select a map") # Check if selected layers are present in the active map input_layers_list = input_layers.split(";") layers_to_render = [] for input_layer in input_layers_list: present = False for layer in map_layers: if layer.name == input_layer: layers_to_render.append(layer.name) present = True break if not present: added_layer = active_map.addDataFromPath(input_layer) layers_to_render.append(added_layer.name) # Retrieve styles that are currently in the project project_styles = project.styles # Check if styles are in the project - add them if not present style_files_list = style_files.split(";") for style_path in style_files_list: if not style_path in project_styles: # Add the style to the project project_styles.append(style_path) project.updateStyles(project_styles) # Retrieve symbol codes from legend table symbol_codes = {} with arcpy.da.SearchCursor( table, [ primary_key_field, fill_symbol_code_field, line_symbol_code_field, point_symbol_code_field, ], ) as search_cursor: for row in search_cursor: symbol_codes[str(row[0])] = { "fill": row[1], "stroke": row[2], "marker": row[3], } # Retrieve label texts for custom labels add_labels = False if label_field_1 or label_field_2: add_labels = True custom_labels = {} labels = [primary_key_field] if label_field_1: labels.append(label_field_1) if label_field_2: labels.append(label_field_2) with arcpy.da.SearchCursor( table, labels, ) as search_cursor: for row in search_cursor: custom_labels[row[0]] = f"{row[1]}{label_delimieter}{row[2]}" # Retrieve headings headings = {} if heading_field: labels = [primary_key_field, heading_field] with arcpy.da.SearchCursor( table, labels, ) as search_cursor: for row in search_cursor: headings[str(row[0])] = row[1] # Retrieve outline color codes outline_codes = {} if outline_field: with arcpy.da.SearchCursor( table, [primary_key_field, outline_field], ) as search_cursor: for row in search_cursor: outline_codes[str(row[0])] = row[1] # Start rendering process for layer in map_layers: if layer.name in layers_to_render: messages.AddMessage(f"Rendering layer: {layer.name}") sym = layer.symbology layer_desc = arcpy.Describe(layer) layer_shape_type = layer_desc.featureClass.shapeType # Check if UniqueValueRenderer is defined if sym.renderer == "UniqueValueRenderer": # Apply symbols from gallery for group in sym.renderer.groups: for item in group.items: if item.values[0][0] != "": leg_id = str(item.values[0][0]) else: leg_id = None if leg_id in symbol_codes: symbol_code = get_symbol_code_for_shape( layer_shape_type, symbol_codes[leg_id] ) if symbol_code == "#": continue code_components = get_code_components(symbol_code) symbol_key = code_components["symbol_key"] if symbol_key: symbols_from_gallery = ( item.symbol.listSymbolsFromGallery(symbol_key) ) symbol_found = False for symbol_from_gallery in symbols_from_gallery: if symbol_from_gallery.name == symbol_key: symbol_found = True item.symbol = symbol_from_gallery break # Print message if symbol could not be found if not symbol_found: messages.AddMessage( f"Could not find symbol in gallery {symbol_key}" ) # Add user defined labels if add_labels: if leg_id in custom_labels: item.label = custom_labels[leg_id] layer.symbology = sym # Retrieve CIM to add colors cim_lyr = layer.getDefinition("V3") # Retrieve stroke symbol properties stroke_symbol_props = {} for group in cim_lyr.renderer.groups: for unique_value_class in group.classes: if unique_value_class.values[0].fieldValues[0] != "": leg_id = str( unique_value_class.values[0].fieldValues[0] ) else: leg_id = None stroke_symbol_props[leg_id] = { "color": None, "width": None, } for ( symbol_layer ) in unique_value_class.symbol.symbol.symbolLayers: if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke, ): if leg_id in outline_codes: # Set user defined outline color outline_code = outline_codes[leg_id] color_value = get_symbol_color(outline_code) color = arcpy.cim.CreateCIMObjectFromClassName( "CIMCMYKColor", "V3" ) color.values = color_value["CMYK"] stroke_symbol_props[leg_id]["color"] = color else: if symbol_layer.color: # Set color as it was before stroke_symbol_props[leg_id][ "color" ] = symbol_layer.color else: # Set default color color = ( arcpy.cim.CreateCIMObjectFromClassName( "CIMCMYKColor", "V3" ) ) color.values = [40, 40, 40, 10, 100] stroke_symbol_props[leg_id]["color"] = color if draw_outlines: if symbol_layer.width: # Set width as it was before stroke_symbol_props[leg_id][ "width" ] = symbol_layer.width else: # Set default width stroke_symbol_props[leg_id][ "width" ] = DEFAULT_LINE_WIDTH else: stroke_symbol_props[leg_id]["width"] = 0 break # In case the layer did not have a stroke symbol layer or outline color if not stroke_symbol_props[leg_id]["color"]: if leg_id in outline_codes: messages.AddMessage("Add color") # Set user defined outline color outline_code = outline_codes[leg_id] color_value = get_symbol_color(outline_code) color = arcpy.cim.CreateCIMObjectFromClassName( "CIMCMYKColor", "V3" ) color.values = color_value["CMYK"] stroke_symbol_props[leg_id]["color"] = color else: # Set default color color = arcpy.cim.CreateCIMObjectFromClassName( "CIMCMYKColor", "V3" ) color.values = [40, 40, 40, 10, 100] stroke_symbol_props[leg_id]["color"] = color if not stroke_symbol_props[leg_id]["width"]: if draw_outlines: stroke_symbol_props[leg_id][ "width" ] = DEFAULT_LINE_WIDTH else: stroke_symbol_props[leg_id]["width"] = 0 new_groups = [] for group in cim_lyr.renderer.groups: for unique_value_class in group.classes: if unique_value_class.values[0].fieldValues[0] != "": leg_id = str( unique_value_class.values[0].fieldValues[0] ) else: leg_id = None if leg_id in symbol_codes: symbol_code = get_symbol_code_for_shape( layer_shape_type, symbol_codes[leg_id] ) if symbol_code == "#": continue code_components = get_code_components(symbol_code) color_value = code_components["color"] symbol_color_value = code_components["symbol_color"] has_color = False for ( symbol_layer ) in unique_value_class.symbol.symbol.symbolLayers: if symbol_color_value: update_symbol_layer_colors( symbol_layer, symbol_color_value ) if color_value: # Update colors if layer_shape_type == "Polygon": if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMSolidFill, ): update_color(symbol_layer, color_value) has_color = True if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke, ): symbol_layer.width = ( stroke_symbol_props[leg_id]["width"] ) symbol_layer.color = ( stroke_symbol_props[leg_id]["color"] ) elif layer_shape_type == "Polyline": if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke, ): update_color(symbol_layer, color_value) has_color = True elif layer_shape_type == "Point": if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMMarker, ): update_color(symbol_layer, color_value) has_color = True # Draw symbol background colors if not has_color and color_value: if layer_shape_type == "Polygon": # Add fill symbol layer fill_symbol = arcpy.cim.CIMSolidFill() update_color(fill_symbol, color_value) unique_value_class.symbol.symbol.symbolLayers.append( fill_symbol ) # Add stroke symbol layer if stroke_symbol_props[leg_id]: stroke_symbol = arcpy.cim.CIMSolidStroke() stroke_symbol.color = stroke_symbol_props[ leg_id ]["color"] stroke_symbol.width = stroke_symbol_props[ leg_id ]["width"] unique_value_class.symbol.symbol.symbolLayers.append( stroke_symbol ) elif layer_shape_type == "Polyline": # Add stroke symbol layer stroke_symbol = arcpy.cim.CIMSolidStroke() update_color(stroke_symbol, color_value) unique_value_class.symbol.symbol.symbolLayers.append( stroke_symbol ) elif layer_shape_type == "Point": # Add marker symbol layer marker_symbol = arcpy.cim.CIMMarker() update_color(marker_symbol, color_value) unique_value_class.symbol.symbol.symbolLayers.append( marker_symbol ) # Regroup items if heading_field: if leg_id in headings: heading = headings[leg_id] if not heading: heading = "Other" else: heading = "Other" matched_group = next( ( group for group in new_groups if group.heading == heading ), None, ) if matched_group: matched_group.classes.append(unique_value_class) else: new_group = arcpy.cim.CreateCIMObjectFromClassName( "CIMUniqueValueGroup", "V3" ) new_group.heading = heading new_group.classes.append(unique_value_class) new_groups.append(new_group) if heading_field: cim_lyr.renderer.groups = new_groups # Push changes back to layer object layer.setDefinition(cim_lyr) else: raise ValueError( f"Execution aborted: please apply a Unique Value Renderer to layer {layer.name}" ) def postExecute(self, _): return def update_color(symbol_layer, color_value): color = arcpy.cim.CreateCIMObjectFromClassName("CIMCMYKColor", "V3") color.values = color_value["CMYK"] symbol_layer.color = color def update_symbol_layer_colors(symbol_layer, symbol_color_value): if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMCharacterMarker, ): for sub_symbol_layer in symbol_layer.symbol.symbolLayers: update_color(sub_symbol_layer, symbol_color_value) if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMVectorMarker, ): for marker_graphic in symbol_layer.markerGraphics: for sub_symbol_layer in marker_graphic.symbol.symbolLayers: update_color(sub_symbol_layer, symbol_color_value) def get_symbol_code_for_shape(shape_type, symbol_codes): if shape_type == "Polygon": return symbol_codes["fill"] elif shape_type == "Polyline": return symbol_codes["stroke"] elif shape_type == "Point": return symbol_codes["marker"] else: raise ValueError( "Execute error: unknown shape type - allowed types: Point, Polyline and Polygon" ) def get_code_components(code): code_len = len(code) if code_len == 4: return { "color": decode_color(code), "symbol_key": "", "symbol_color": {}, } elif code_len == 10: return { "color": {}, "symbol_key": code[0:7], "symbol_color": get_symbol_color(code[7:]), } elif code_len == 11: return { "color": decode_color(code[:4]), "symbol_key": code[4:12], "symbol_color": {}, } elif code_len == 14: return { "color": decode_color(code[:4]), "symbol_key": code[4:11], "symbol_color": get_symbol_color(code[11:]), } else: raise ValueError(f"Execution aborted: unknown symbol code {code}") def decode_color(color_string): return { "CMYK": [ get_percentage_from_letter(color_string[0]), get_percentage_from_letter(color_string[1]), get_percentage_from_letter(color_string[2]), get_percentage_from_letter(color_string[3]), 100, ], } def get_percentage_from_letter(letter): if letter == "F": return 15 elif letter == "V": return 100 elif letter == "S": return 6 elif letter == "X": return 0 else: try: return int(letter) * 10 except ValueError as _: raise ValueError(f"Execution aborted: unknown color code {letter}") def get_symbol_color(color_string): if color_string == "BLK": return { "CMYK": [0, 0, 0, 100, 100], } elif color_string == "ROT": return { "CMYK": [0, 100, 100, 21, 100], } elif color_string == "CYN": return { "CMYK": [100, 0, 0, 0, 100], } elif color_string == "MGT": return { "CMYK": [0, 100, 0, 0, 100], } elif color_string == "BLA": return { "CMYK": [70, 30, 0, 21, 100], } elif color_string == "BRN": return { "CMYK": [45, 73, 93, 21, 100], } elif color_string == "GRN": return { "CMYK": [70, 0, 70, 21, 100], } elif color_string == "GLB": return { "CMYK": [0, 0, 100, 0, 100], } elif color_string == "ORA": return { "CMYK": [0, 32.16, 100, 0, 100], } elif color_string == "WHT": return { "CMYK": [100, 100, 100, 0, 100], } else: raise ValueError(f"Execution aborted: unknown color code {color_string}")