diff --git a/FeatureRenderer.pyt b/FeatureRenderer.pyt index cdde665..c1f4330 100644 --- a/FeatureRenderer.pyt +++ b/FeatureRenderer.pyt @@ -2,7 +2,10 @@ import arcpy # type: ignore -DEFAULT_LINE_WIDTH = 0.7 +# Define constants +# 1 point = 1/72 inch +# 1 inch = 25.4 mm +DEFAULT_LINE_WIDTH = 0.23 DEFAULT_COLOR = [ 0, 0, @@ -62,7 +65,7 @@ class FeatureRenderer: # Define parameter for selecting a primary key field_param_1 = arcpy.Parameter( - displayName="Select Primary Key Field (LEG_ID)", + displayName="Select Primary Key Field", name="primary_key", datatype="Field", parameterType="Required", @@ -204,6 +207,18 @@ class FeatureRenderer: outline_field.parameterDependencies = [table_param.name] + # Define parameter for outline width + outline_width = arcpy.Parameter( + name="outline_width", + displayName="Set outline width in pt", + datatype="GPDouble", + direction="Input", + parameterType="Optional", + category="Define Outlines", + ) + + outline_field.parameterDependencies = [table_param.name] + # Return parameter definitions as a list return [ style_files, @@ -219,6 +234,7 @@ class FeatureRenderer: heading_field, draw_outlines, outline_field, + outline_width, ] def isLicensed(self): @@ -248,6 +264,7 @@ class FeatureRenderer: heading_field = parameters[10].valueAsText draw_outlines = parameters[11].value outline_field = parameters[12].valueAsText + outline_width = parameters[13].value # Retrieve currently active map active_map = project.activeMap @@ -264,15 +281,20 @@ class FeatureRenderer: layers_to_render = [] for input_layer in input_layers_list: present = False + + # Test if layer is a group layer and extract layer name from path + if "\\" in input_layer: + input_layer = input_layer.split("\\")[-1] + for layer in map_layers: if layer.name == input_layer: - layers_to_render.append(layer.name) + layers_to_render.append(layer) present = True break if not present: added_layer = active_map.addDataFromPath(input_layer) - layers_to_render.append(added_layer.name) + layers_to_render.append(added_layer) # Retrieve styles that are currently in the project project_styles = project.styles @@ -305,32 +327,49 @@ class FeatureRenderer: # Retrieve label texts for custom labels add_labels = False + custom_labels = {} 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) + fields = [primary_key_field] + if label_field_1 and label_field_1 not in fields: + fields.append(label_field_1) + if label_field_2 and label_field_2 not in fields: + fields.append(label_field_2) with arcpy.da.SearchCursor( table, - labels, + fields, ) as search_cursor: for row in search_cursor: - custom_labels[row[0]] = f"{row[1]}{label_delimieter}{row[2]}" + id = str(row[0]) + if id not in custom_labels: + if len(fields) == 3: + custom_labels[id] = f"{row[1]}{label_delimieter}{row[2]}" + elif len(fields) == 2: + if label_field_1 == primary_key_field: + custom_labels[id] = ( + f"{row[0]}{label_delimieter}{row[1]}" + ) + else: + custom_labels[id] = f"{row[1]}" + elif len(fields) == 1: + custom_labels[id] = f"{row[0]}" # Retrieve headings headings = {} if heading_field: - labels = [primary_key_field, heading_field] + fields = [primary_key_field] + if heading_field not in fields: + fields.append(heading_field) with arcpy.da.SearchCursor( table, - labels, + fields, ) as search_cursor: for row in search_cursor: - headings[str(row[0])] = row[1] + if len(fields) == 2: + headings[str(row[0])] = row[1] + elif len(fields) == 1: + headings[str(row[0])] = row[0] # Retrieve outline color codes outline_codes = {} @@ -343,153 +382,95 @@ class FeatureRenderer: 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}") + for layer 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 + 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]) + # 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, leg_id) + 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: + arcpy.AddWarning( + f"Could not find symbol in gallery {symbol_key}" + ) + else: + arcpy.AddWarning(f"{leg_id} is not in legend table") + + # 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 = {} + if layer_shape_type == "Polygon": + 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] - ) + stroke_symbol_props[leg_id] = { + "color": None, + "width": None, + } - 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}" - ) - else: - messages.AddMessage( - f"{leg_id} is not in legend table yet" - ) - - # 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 = {} - if layer_shape_type == "Polygon": - for group in cim_lyr.renderer.groups: - for unique_value_class in group.classes: - if ( - unique_value_class.values[0].fieldValues[0] - != "" + for ( + symbol_layer + ) in unique_value_class.symbol.symbol.symbolLayers: + if isinstance( + symbol_layer, + arcpy.cim.CIMSymbols.CIMSolidStroke, ): - 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] - - if ( - not outline_code - ) or outline_code == "#": - outline_code = CODE_BLK - - 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 = DEFAULT_COLOR - 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: # Set user defined outline color outline_code = outline_codes[leg_id] + if outline_code and outline_code != "#": + color_value = get_symbol_color(outline_code) + else: + color_value = {"CMYK": DEFAULT_COLOR} - if (not outline_code) or outline_code == "#": - outline_code = CODE_BLK - - color_value = get_symbol_color(outline_code) color = arcpy.cim.CreateCIMObjectFromClassName( "CIMCMYKColor", "V3" ) @@ -503,130 +484,156 @@ class FeatureRenderer: color.values = DEFAULT_COLOR stroke_symbol_props[leg_id]["color"] = color - if not stroke_symbol_props[leg_id]["width"]: - if draw_outlines: + break + + # In case the layer did not have a stroke symbol layer + if not stroke_symbol_props[leg_id]["color"]: + if leg_id in outline_codes: + # Set user defined outline color + outline_code = outline_codes[leg_id] + if outline_code and outline_code != "#": + color_value = get_symbol_color(outline_code) + else: + color_value = {"CMYK": DEFAULT_COLOR} + + 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 = DEFAULT_COLOR + stroke_symbol_props[leg_id]["color"] = color + + # Set outline width + if draw_outlines: + if outline_width: + stroke_symbol_props[leg_id]["width"] = outline_width + else: + if not stroke_symbol_props[leg_id]["width"]: 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 + stroke_symbol_props[leg_id]["width"] = 0 - if leg_id in symbol_codes: - symbol_code = get_symbol_code_for_shape( - layer_shape_type, symbol_codes[leg_id] - ) - if symbol_code == "#": - continue + new_groups = [] + for group in cim_lyr.renderer.groups: + for unique_value_class in group.classes: - code_components = get_code_components(symbol_code) - color_value = code_components["color"] - symbol_color_value = code_components["symbol_color"] + if unique_value_class.values[0].fieldValues[0] != "": + leg_id = str(unique_value_class.values[0].fieldValues[0]) + else: + leg_id = None - has_background_color = False - for ( - symbol_layer - ) in unique_value_class.symbol.symbol.symbolLayers: + if leg_id in symbol_codes: + symbol_code = get_symbol_code_for_shape( + layer_shape_type, symbol_codes[leg_id] + ) + if symbol_code == "#": + continue - if symbol_color_value: - update_symbol_layer_colors( - symbol_layer, symbol_color_value - ) + code_components = get_code_components(symbol_code, leg_id) + color_value = code_components["color"] + symbol_color_value = code_components["symbol_color"] - if color_value: - # Update background colors - if layer_shape_type == "Polygon": - if isinstance( - symbol_layer, - arcpy.cim.CIMSymbols.CIMSolidFill, - ): - update_color(symbol_layer, color_value) - has_background_color = True + has_background_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 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"] - ) - - # Draw symbol background colors - if not has_background_color and color_value: + if color_value: + # Update background colors if layer_shape_type == "Polygon": - # Add fill symbol layer - fill_symbol = arcpy.cim.CIMSolidFill() - update_color(fill_symbol, color_value) + if isinstance( + symbol_layer, + arcpy.cim.CIMSymbols.CIMSolidFill, + ): + update_color(symbol_layer, color_value) + has_background_color = True - 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[ + 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"] - # Insert at the beginning such that it appears on top of solid fill layer - unique_value_class.symbol.symbol.symbolLayers.insert( - 0, stroke_symbol - ) + # Draw symbol background colors + if not has_background_color and color_value: + if layer_shape_type == "Polygon": + # Add fill symbol layer + fill_symbol = arcpy.cim.CIMSolidFill() + update_color(fill_symbol, color_value) - # 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" + unique_value_class.symbol.symbol.symbolLayers.append( + fill_symbol ) - 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 + # 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"] - # Push changes back to layer object - layer.setDefinition(cim_lyr) + # Insert at the beginning such that it appears on top of solid fill layer + unique_value_class.symbol.symbol.symbolLayers.insert( + 0, stroke_symbol + ) - else: - raise ValueError( - f"Execution aborted: please apply a Unique Value Renderer to layer {layer.name}" - ) + # 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 @@ -642,6 +649,12 @@ def update_symbol_layer_colors(symbol_layer, symbol_color_value): if isinstance(symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke): update_color(symbol_layer, symbol_color_value) + if isinstance(symbol_layer, arcpy.cim.CIMSymbols.CIMHatchFill) and hasattr( + symbol_layer, "lineSymbol" + ): + for sub_symbol_layer in symbol_layer.lineSymbol.symbolLayers: + update_color(sub_symbol_layer, symbol_color_value) + if isinstance( symbol_layer, arcpy.cim.CIMSymbols.CIMCharacterMarker, @@ -671,11 +684,12 @@ def get_symbol_code_for_shape(shape_type, symbol_codes): ) -def get_code_components(code): +# code has three components: fill color, symbol key, symbol color +def get_code_components(code, leg_id): code_len = len(code) if code_len == 4: return { - "color": decode_color(code), + "color": decode_color(code, leg_id), "symbol_key": "", "symbol_color": {}, } @@ -695,13 +709,13 @@ def get_code_components(code): } elif code_len == 11: return { - "color": decode_color(code[:4]), + "color": decode_color(code[:4], leg_id), "symbol_key": code[4:12], "symbol_color": {}, } elif code_len == 14: return { - "color": decode_color(code[:4]), + "color": decode_color(code[:4], leg_id), "symbol_key": code[4:11], "symbol_color": get_symbol_color(code[11:]), } @@ -709,19 +723,29 @@ def get_code_components(code): 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 decode_color(color_string, leg_id): + if color_string[3] == "X": + return { + "CMYK": [ + 0, + 0, + 0, + 0, + ], + } + else: + return { + "CMYK": [ + get_percentage_from_letter(color_string[0], leg_id), + get_percentage_from_letter(color_string[1], leg_id), + get_percentage_from_letter(color_string[2], leg_id), + get_percentage_from_letter(color_string[3], leg_id), + 100, + ], + } -def get_percentage_from_letter(letter): +def get_percentage_from_letter(letter, leg_id): if letter == "F": return 15 elif letter == "V": @@ -733,8 +757,10 @@ def get_percentage_from_letter(letter): else: try: return int(letter) * 10 - except ValueError as _: - raise ValueError(f"Execution aborted: unknown color code {letter}") + except Exception as _: + raise ValueError( + f"Execution aborted: unknown color code {letter} for {leg_id}" + ) def get_symbol_color(color_string): @@ -778,5 +804,7 @@ def get_symbol_color(color_string): return { "CMYK": [100, 100, 100, 0, 100], } + elif color_string == "GRA": + return {"CMYK": [10, 10, 10, 21, 100]} else: raise ValueError(f"Execution aborted: unknown color code {color_string}") diff --git a/README.md b/README.md index 40db242..e2882c1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Feature Renderer -This is a Python toolbox for rendering geologic features in ArcGIS Pro. The toolbox is based on [ArcPy](https://pro.arcgis.com/en/pro-app/latest/arcpy/main/arcgis-pro-arcpy-reference.htm) modules. Specifically, it is based on the Cartographic Information Model [CIM](https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm) to manipulate symbol layers and their properties. +This is a Python toolbox for rendering of geologic features in ArcGIS Pro. The toolbox is based on [ArcPy](https://pro.arcgis.com/en/pro-app/latest/arcpy/main/arcgis-pro-arcpy-reference.htm). Specifically, it uses the Cartographic Information Model [CIM](https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm) to manipulate symbol layers and their properties. ## Usage