feature-renderer/FeatureRenderer.pyt

770 lines
30 KiB
Python

# -*- 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] != "<Null>":
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] != "<Null>":
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] != "<Null>":
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}")