commit 7165f69f330c63cbf4c3cc1b9a9714b2e686676f Author: Thomas Fuhrmann Date: Wed Nov 20 10:01:54 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96c0ecc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +docs/ \ No newline at end of file diff --git a/FeatureRenderer.pyt b/FeatureRenderer.pyt new file mode 100644 index 0000000..9c08b88 --- /dev/null +++ b/FeatureRenderer.pyt @@ -0,0 +1,614 @@ +# -*- coding: utf-8 -*- + +import arcpy # type: ignore + + +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="DETable", + 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 stroke symbol code field + field_param_3 = arcpy.Parameter( + displayName="Select Stroke 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] + + # 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, + ] + + 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 + + # 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[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]}" + + if heading_field: + labels = [primary_key_field, heading_field] + headings = {} + with arcpy.da.SearchCursor( + table, + labels, + ) as search_cursor: + for row in search_cursor: + headings[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": + + # Regroup items according to heading field + if heading_field: + new_item_groups = {} + old_item_groups = {} + + for group in sym.renderer.groups: + old_item_groups[group.heading] = group.items + + for group in sym.renderer.groups: + for item in group.items: + if item.values[0][0] != "": + leg_id = int(item.values[0][0]) + else: + leg_id = None + + if leg_id in headings: + heading = headings[leg_id] + if not heading: + heading = "Other" + else: + heading = "Other" + + if heading in new_item_groups: + new_item_groups[heading].append(item) + else: + new_item_groups[heading] = [item] + + # Remove old item groups + sym.renderer.removeValues(old_item_groups) + layer.symbology = sym + + # Add new item groups + sym.renderer.addValues(new_item_groups) + layer.symbology = sym + + # Apply symbols from gallery + for group in sym.renderer.groups: + for item in group.items: + if item.values[0][0] != "": + leg_id = int(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 leg_id == 11804: + messages.AddMessage(symbol_key) + + if symbol_key: + symbols_from_gallery = ( + item.symbol.listSymbolsFromGallery(symbol_key) + ) + for symbol_from_gallery in symbols_from_gallery: + if symbol_from_gallery.name == symbol_key: + item.symbol = symbol_from_gallery + break + + # 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") + + for group in cim_lyr.renderer.groups: + for unique_value_class in group.classes: + + if unique_value_class.values[0].fieldValues[0] != "": + leg_id = int( + 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 + + 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 + + if not has_color and color_value: + if layer_shape_type == "Polygon": + # Create CIMSolidFill + fill_symbol = arcpy.cim.CIMSolidFill() + update_color(fill_symbol, color_value) + unique_value_class.symbol.symbol.symbolLayers.append( + fill_symbol + ) + elif layer_shape_type == "Polyline": + # Create CIMSolidStroke + 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": + # Create CIMMarker + marker_symbol = arcpy.cim.CIMMarker() + update_color(marker_symbol, color_value) + unique_value_class.symbol.symbol.symbolLayers.append( + marker_symbol + ) + + # 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 + + +# Update color property of symbol layer +def update_color(symbol_layer, color_value): + color = arcpy.cim.CreateCIMObjectFromClassName("CIMCMYKColor", "V3") + color.values = color_value["CMYK"] + symbol_layer.color = color + + +# Update colors of symbol layers of type CIMCharacterMarker and CIMVectorMarker +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) + + +# Retrieve symbole code for shape type +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" + ) + + +# Decode symbol code +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}") + + +# Decode color values +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, + ], + } + + +# Decode letters +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: + return int(letter) * 10 + + +# Decode symbol colors +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}") diff --git a/README.md b/README.md new file mode 100644 index 0000000..40db242 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# 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. + +## Usage + +Copy this file to your project folder and add the toolbox to your project. Style layers are automatically added to your project. + +## Contact information + +If you have any questions related to this code, please contact [Department Geoinformation](mailto:gis@geosphere.at).