Compare commits

..

No commits in common. "6c562d15eaea25f054bd969998f47b10da65ee14" and "e8d04ada0576213b7e6193c4dabf2657fff966f9" have entirely different histories.

2 changed files with 277 additions and 305 deletions

View file

@ -2,10 +2,7 @@
import arcpy # type: ignore import arcpy # type: ignore
# Define constants DEFAULT_LINE_WIDTH = 0.7
# 1 point = 1/72 inch
# 1 inch = 25.4 mm
DEFAULT_LINE_WIDTH = 0.23
DEFAULT_COLOR = [ DEFAULT_COLOR = [
0, 0,
0, 0,
@ -65,7 +62,7 @@ class FeatureRenderer:
# Define parameter for selecting a primary key # Define parameter for selecting a primary key
field_param_1 = arcpy.Parameter( field_param_1 = arcpy.Parameter(
displayName="Select Primary Key Field", displayName="Select Primary Key Field (LEG_ID)",
name="primary_key", name="primary_key",
datatype="Field", datatype="Field",
parameterType="Required", parameterType="Required",
@ -207,18 +204,6 @@ class FeatureRenderer:
outline_field.parameterDependencies = [table_param.name] 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 parameter definitions as a list
return [ return [
style_files, style_files,
@ -234,7 +219,6 @@ class FeatureRenderer:
heading_field, heading_field,
draw_outlines, draw_outlines,
outline_field, outline_field,
outline_width,
] ]
def isLicensed(self): def isLicensed(self):
@ -264,7 +248,6 @@ class FeatureRenderer:
heading_field = parameters[10].valueAsText heading_field = parameters[10].valueAsText
draw_outlines = parameters[11].value draw_outlines = parameters[11].value
outline_field = parameters[12].valueAsText outline_field = parameters[12].valueAsText
outline_width = parameters[13].value
# Retrieve currently active map # Retrieve currently active map
active_map = project.activeMap active_map = project.activeMap
@ -281,20 +264,15 @@ class FeatureRenderer:
layers_to_render = [] layers_to_render = []
for input_layer in input_layers_list: for input_layer in input_layers_list:
present = False 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: for layer in map_layers:
if layer.name == input_layer: if layer.name == input_layer:
layers_to_render.append(layer) layers_to_render.append(layer.name)
present = True present = True
break break
if not present: if not present:
added_layer = active_map.addDataFromPath(input_layer) added_layer = active_map.addDataFromPath(input_layer)
layers_to_render.append(added_layer) layers_to_render.append(added_layer.name)
# Retrieve styles that are currently in the project # Retrieve styles that are currently in the project
project_styles = project.styles project_styles = project.styles
@ -327,49 +305,32 @@ class FeatureRenderer:
# Retrieve label texts for custom labels # Retrieve label texts for custom labels
add_labels = False add_labels = False
custom_labels = {}
if label_field_1 or label_field_2: if label_field_1 or label_field_2:
add_labels = True add_labels = True
fields = [primary_key_field] custom_labels = {}
if label_field_1 and label_field_1 not in fields: labels = [primary_key_field]
fields.append(label_field_1) if label_field_1:
if label_field_2 and label_field_2 not in fields: labels.append(label_field_1)
fields.append(label_field_2) if label_field_2:
labels.append(label_field_2)
with arcpy.da.SearchCursor( with arcpy.da.SearchCursor(
table, table,
fields, labels,
) as search_cursor: ) as search_cursor:
for row in search_cursor: for row in search_cursor:
id = str(row[0]) custom_labels[row[0]] = f"{row[1]}{label_delimieter}{row[2]}"
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 # Retrieve headings
headings = {} headings = {}
if heading_field: if heading_field:
fields = [primary_key_field] labels = [primary_key_field, heading_field]
if heading_field not in fields:
fields.append(heading_field)
with arcpy.da.SearchCursor( with arcpy.da.SearchCursor(
table, table,
fields, labels,
) as search_cursor: ) as search_cursor:
for row in search_cursor: for row in search_cursor:
if len(fields) == 2: headings[str(row[0])] = row[1]
headings[str(row[0])] = row[1]
elif len(fields) == 1:
headings[str(row[0])] = row[0]
# Retrieve outline color codes # Retrieve outline color codes
outline_codes = {} outline_codes = {}
@ -382,95 +343,153 @@ class FeatureRenderer:
outline_codes[str(row[0])] = row[1] outline_codes[str(row[0])] = row[1]
# Start rendering process # Start rendering process
for layer in layers_to_render: for layer in map_layers:
messages.AddMessage(f"Rendering layer: {layer.name}") if layer.name in layers_to_render:
messages.AddMessage(f"Rendering layer: {layer.name}")
sym = layer.symbology sym = layer.symbology
layer_desc = arcpy.Describe(layer) layer_desc = arcpy.Describe(layer)
layer_shape_type = layer_desc.featureClass.shapeType layer_shape_type = layer_desc.featureClass.shapeType
# Check if UniqueValueRenderer is defined # Check if UniqueValueRenderer is defined
if sym.renderer == "UniqueValueRenderer": if sym.renderer == "UniqueValueRenderer":
# Apply symbols from gallery # Apply symbols from gallery
for group in sym.renderer.groups: for group in sym.renderer.groups:
for item in group.items: for item in group.items:
if item.values[0][0] != "<Null>": if item.values[0][0] != "<Null>":
leg_id = str(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] != "<Null>":
leg_id = str(
unique_value_class.values[0].fieldValues[0]
)
else: else:
leg_id = None leg_id = None
stroke_symbol_props[leg_id] = { if leg_id in symbol_codes:
"color": None, symbol_code = get_symbol_code_for_shape(
"width": None, layer_shape_type, symbol_codes[leg_id]
} )
for ( if symbol_code == "#":
symbol_layer continue
) in unique_value_class.symbol.symbol.symbolLayers:
if isinstance( code_components = get_code_components(symbol_code)
symbol_layer, symbol_key = code_components["symbol_key"]
arcpy.cim.CIMSymbols.CIMSolidStroke,
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]
!= "<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]
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: if leg_id in outline_codes:
# Set user defined outline color # Set user defined outline color
outline_code = outline_codes[leg_id] 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( color = arcpy.cim.CreateCIMObjectFromClassName(
"CIMCMYKColor", "V3" "CIMCMYKColor", "V3"
) )
@ -484,156 +503,130 @@ class FeatureRenderer:
color.values = DEFAULT_COLOR color.values = DEFAULT_COLOR
stroke_symbol_props[leg_id]["color"] = color stroke_symbol_props[leg_id]["color"] = color
break if not stroke_symbol_props[leg_id]["width"]:
if draw_outlines:
# 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][ stroke_symbol_props[leg_id][
"width" "width"
] = DEFAULT_LINE_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: else:
stroke_symbol_props[leg_id]["width"] = 0 leg_id = None
new_groups = [] if leg_id in symbol_codes:
for group in cim_lyr.renderer.groups: symbol_code = get_symbol_code_for_shape(
for unique_value_class in group.classes: layer_shape_type, symbol_codes[leg_id]
)
if symbol_code == "#":
continue
if unique_value_class.values[0].fieldValues[0] != "<Null>": code_components = get_code_components(symbol_code)
leg_id = str(unique_value_class.values[0].fieldValues[0]) color_value = code_components["color"]
else: symbol_color_value = code_components["symbol_color"]
leg_id = None
if leg_id in symbol_codes: has_background_color = False
symbol_code = get_symbol_code_for_shape( for (
layer_shape_type, symbol_codes[leg_id] symbol_layer
) ) in unique_value_class.symbol.symbol.symbolLayers:
if symbol_code == "#":
continue
code_components = get_code_components(symbol_code, leg_id) if symbol_color_value:
color_value = code_components["color"] update_symbol_layer_colors(
symbol_color_value = code_components["symbol_color"] symbol_layer, symbol_color_value
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 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
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 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"]
# Insert at the beginning such that it appears on top of solid fill layer
unique_value_class.symbol.symbol.symbolLayers.insert(
0, stroke_symbol
) )
# Regroup items if color_value:
if heading_field: # Update background colors
if leg_id in headings: if layer_shape_type == "Polygon":
heading = headings[leg_id] if isinstance(
if not heading: symbol_layer,
arcpy.cim.CIMSymbols.CIMSolidFill,
):
update_color(symbol_layer, color_value)
has_background_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"]
)
# 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)
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"]
# Insert at the beginning such that it appears on top of solid fill layer
unique_value_class.symbol.symbol.symbolLayers.insert(
0, stroke_symbol
)
# Regroup items
if heading_field:
if leg_id in headings:
heading = headings[leg_id]
if not heading:
heading = "Other"
else:
heading = "Other" heading = "Other"
else:
heading = "Other"
matched_group = next( matched_group = next(
( (
group group
for group in new_groups for group in new_groups
if group.heading == heading if group.heading == heading
), ),
None, None,
)
if matched_group:
matched_group.classes.append(unique_value_class)
else:
new_group = arcpy.cim.CreateCIMObjectFromClassName(
"CIMUniqueValueGroup", "V3"
) )
new_group.heading = heading if matched_group:
new_group.classes.append(unique_value_class) matched_group.classes.append(unique_value_class)
new_groups.append(new_group) 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: if heading_field:
cim_lyr.renderer.groups = new_groups cim_lyr.renderer.groups = new_groups
# Push changes back to layer object # Push changes back to layer object
layer.setDefinition(cim_lyr) layer.setDefinition(cim_lyr)
else: else:
raise ValueError( raise ValueError(
f"Execution aborted: please apply a Unique Value Renderer to layer {layer.name}" f"Execution aborted: please apply a Unique Value Renderer to layer {layer.name}"
) )
def postExecute(self, _): def postExecute(self, _):
return return
@ -649,12 +642,6 @@ def update_symbol_layer_colors(symbol_layer, symbol_color_value):
if isinstance(symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke): if isinstance(symbol_layer, arcpy.cim.CIMSymbols.CIMSolidStroke):
update_color(symbol_layer, symbol_color_value) 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( if isinstance(
symbol_layer, symbol_layer,
arcpy.cim.CIMSymbols.CIMCharacterMarker, arcpy.cim.CIMSymbols.CIMCharacterMarker,
@ -684,12 +671,11 @@ def get_symbol_code_for_shape(shape_type, symbol_codes):
) )
# code has three components: fill color, symbol key, symbol color def get_code_components(code):
def get_code_components(code, leg_id):
code_len = len(code) code_len = len(code)
if code_len == 4: if code_len == 4:
return { return {
"color": decode_color(code, leg_id), "color": decode_color(code),
"symbol_key": "", "symbol_key": "",
"symbol_color": {}, "symbol_color": {},
} }
@ -709,13 +695,13 @@ def get_code_components(code, leg_id):
} }
elif code_len == 11: elif code_len == 11:
return { return {
"color": decode_color(code[:4], leg_id), "color": decode_color(code[:4]),
"symbol_key": code[4:12], "symbol_key": code[4:12],
"symbol_color": {}, "symbol_color": {},
} }
elif code_len == 14: elif code_len == 14:
return { return {
"color": decode_color(code[:4], leg_id), "color": decode_color(code[:4]),
"symbol_key": code[4:11], "symbol_key": code[4:11],
"symbol_color": get_symbol_color(code[11:]), "symbol_color": get_symbol_color(code[11:]),
} }
@ -723,29 +709,19 @@ def get_code_components(code, leg_id):
raise ValueError(f"Execution aborted: unknown symbol code {code}") raise ValueError(f"Execution aborted: unknown symbol code {code}")
def decode_color(color_string, leg_id): def decode_color(color_string):
if color_string[3] == "X": return {
return { "CMYK": [
"CMYK": [ get_percentage_from_letter(color_string[0]),
0, get_percentage_from_letter(color_string[1]),
0, get_percentage_from_letter(color_string[2]),
0, get_percentage_from_letter(color_string[3]),
0, 100,
], ],
} }
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, leg_id): def get_percentage_from_letter(letter):
if letter == "F": if letter == "F":
return 15 return 15
elif letter == "V": elif letter == "V":
@ -757,10 +733,8 @@ def get_percentage_from_letter(letter, leg_id):
else: else:
try: try:
return int(letter) * 10 return int(letter) * 10
except Exception as _: except ValueError as _:
raise ValueError( raise ValueError(f"Execution aborted: unknown color code {letter}")
f"Execution aborted: unknown color code {letter} for {leg_id}"
)
def get_symbol_color(color_string): def get_symbol_color(color_string):
@ -804,7 +778,5 @@ def get_symbol_color(color_string):
return { return {
"CMYK": [100, 100, 100, 0, 100], "CMYK": [100, 100, 100, 0, 100],
} }
elif color_string == "GRA":
return {"CMYK": [10, 10, 10, 21, 100]}
else: else:
raise ValueError(f"Execution aborted: unknown color code {color_string}") raise ValueError(f"Execution aborted: unknown color code {color_string}")

View file

@ -1,6 +1,6 @@
# Feature Renderer # Feature Renderer
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. 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 ## Usage