Python Blog

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Other Boards in This Place


Latest Activity

(207 Posts)
HaydenWelch
MVP Regular Contributor

After a year of procrastination, I've finally written a script that makes the CIM module sane:

https://github.com/hwelch-fle/cimple

 

The goal here is to generate a new cim module from the existing one with the added benefit of proper typing of dependent attributes and string literal completion. The overall structure and usability of the CIM is also improved by utilizing dataclasses and removing the original inheritance hierarchy in favor of direct attribute definition.

Example:

# Original CIM definition
class CIMChart():
    """
      Provides access to members that control chart properties.
    """
    def __init__(self, *args, **Kwargs):
        self.name = str()
        self.series = []
        self.generalProperties = 'CIMChartGeneralProperties'
        self.legend = 'CIMChartLegend'
        self.axes = []
        self.mapSelectionHandling = ChartMapSelectionHandling.Highlight
        self.metaData = str()
        self.multiSeriesChartProperties = 'CIMMultiSeriesChartProperties'
        self.enableServerSideProcessing = False
        self.chartType = ChartType.Basic

# cimple CIM definition
@dataclass
class CIMChart:
    """https://github.com/Esri/cim-spec/blob/main/docs/v3/CIMCharts.md#cimchart-1

    Provides access to members that control chart properties.
    """
    name: str = ''
    series: list[Any] = dc_field(default_factory=list[Any])
    generalProperties: CIMChartGeneralProperties = dc_field(default_factory=lambda: CIMChartGeneralProperties())
    legend: CIMChartLegend = dc_field(default_factory=lambda: CIMChartLegend())
    axes: list[Any] = dc_field(default_factory=list[Any])
    mapSelectionHandling: ChartMapSelectionHandling = 'Highlight'
    metaData: str = ''
    multiSeriesChartProperties: CIMMultiSeriesChartProperties = dc_field(default_factory=lambda: CIMMultiSeriesChartProperties())
    enableServerSideProcessing: bool = False
    chartType: ChartType = 'Basic'

 

The dc_field factories replace the GetPythonClass call and allow for mutable defaults. There are still some lists and dictionaries that use the Any type since I couldn't infer their type from the original module.

 

Update: 

I would like to pose a question to the community:

When using CIM, do you care about inheritance hierarchy? As in, do you use isinstance checks on the bases of a desired CIM class?

e.g.

x = obj.getDefinition('V3')

if issubclass(x, cim.CIMBasicFeatureLayer):
    ## do thing if Layer type
    ...
elif issubclass(x, cim.CIMDisplayTable):
    ## do thing if Table type
    ...

 

Or do you mostly just target the expected output class

e.g.

x = obj.getDefinition('V3')

if isinstance(x, cim.CIMFeatureLayer):
    ## do thing if Layer instance
    ...
elif isinstance(x, cim.CIMMapTableView):
    ## do thing if Table instance
    ...

 

Currently, this module removes all inheritance from CIM and treats each class as a distinct set of keys. This means keys are duplicated between classes, but I feel that when writing code this is much easier to read as you don't need to traverse a whole hierarchy when referencing the source:

HaydenWelch_0-1767376427979.png

Original CIM with inheritance on right, flat cimple.cim on left

If I was to re-introduce subclass and instance checks, I would likely do so by comparing the keys of the classes:

def __subclasscheck__(self, other: object) -> bool:
    if hasattr(other, '__dataclass_fields__'):
        return set(type(other).__dataclass_fields__).issubset(set(type(self).__dataclass_fields__))
    return super().__subclasscheck__(other)

 

Since most of the time when you're type narrowing CIM, you're really just looking for a specific set of keys, I think this is the most sensible solution. It will likely diverge from the base CIM inheritance model though since simple classes that have only simple attributes like 'name' and 'type' will now be considered to be related.

 

Alternatively, I could re-write the script to honor the inheritance hierarchy, with the drawback that inheritance may cause some weird issues with circular imports (the reason the cim module uses strings is to defer this to runtime). Since right now no classes inherit from any other cim class, it is safe to defer the typing and defaults to runtime where all classes are loaded in and can be initialized.

 

Update 2:

I've gone ahead and implemented a CIMBase metaclass that lives in its own _base.py file:

class CIMBase(type):
    def __subclasscheck__(cls, subclass: type) -> bool:
        return set(getattr(cls, '__dataclass_fields__', 'a')).issubset(set(getattr(subclass, '__dataclass_fields__', 'b')))
    
    def __instancecheck__(self, instance: object) -> bool:
        return issubclass(type(instance), self)

 

All CIM classes will now inherit this metaclass to get attribute based (duck style) instance/subclass checks:

@dataclass
class CIMFeatureLayer(metaclass=CIMBase):
    """https://github.com/Esri/cim-spec/blob/main/docs/v3/CIMVectorLayers.md#cimfeaturelayer-1

    NO DOC
    """
    ...
    
@dataclass
class CIMGeoFeatureLayerBase(metaclass=CIMBase):
    """https://github.com/Esri/cim-spec/blob/main/docs/v3/CIMVectorLayers.md#cimgeofeaturelayerbase-1

    Represents a layer that draws geographic feature data using a renderer.
    """
    ...
    
>>> isinstance(CIMFeatureLayer(), CIMGeoFeatureLayerBase)
True
>>> issubclass(CIMFeatureLayer, CIMGeoFeatureLayerBase)
True

 

Again, this will not reveal the original class hierarchy, but will instead reveal if the checked class (arg 1) has all the attributes defined in arg 2.

 

Update 3:

I've now implemented cimple.conversion that allows conversion between cim, cimple, and json. There are some weird caveats for testing (cim objects can take a string literal or an enumerated integer, so writing a test for that required checking against the enum name attribute). Here's the simple code for the conversion functions:

def cim_to_json(cim_object: object, indent: int=4) -> str:
    """Convert a CIM object into a JSON string"""
    return json.dumps(cim_object, indent=indent, cls=JSONCIMEncoder)

def json_to_cim(cim_json: str) -> Any:
    """Convert a json string into an initialized CIM object"""    
    return json.loads(cim_json, cls=JSONCIMDecoder)

def cim_to_cimple(cim_obj: Any) -> Any:
    if isinstance(cim_obj, list):
        return [cim_to_cimple(o) for o in cim_obj]
    cimple_obj = getattr(cim, cim_obj.__class__.__name__, None)
    if cimple_obj:
        return cimple_obj(**{k: cim_to_cimple(v) for k, v in cim_obj.__dict__.items()})
    return cim_obj

def cimple_to_cim(cimple_obj: Any) -> Any:
    if isinstance(cimple_obj, list):
        return [cimple_to_cim(o) for o in cimple_obj]
    cim_obj = getattr(arcpy_cim, cimple_obj.__class__.__name__, None)
    if cim_obj:
        # CIM objects from the arcpy.cim module cannot be initialized with values
        # We need to initialize the object then update the instance __dict__
        cim_obj = cim_obj()
        cim_obj.__dict__.update({k: cimple_to_cim(v) for k, v in cimple_obj.__dict__.items()})
        return cim_obj
    return cimple_obj

 

I've excluded the JSONCIMDecoder an JSONCIMEncoder classes, but they are exposed in the module root if you need to subclass them for custom encodings. 

more
4 3 504
Clubdebambos
MVP Regular Contributor

Learn how to build a custom ArcGIS Pro script tool that creates rectangular buffers around point features using ArcPy based on user input or from field attributes.

Read more...

more
2 0 459
DanPatterson
MVP Esteemed Contributor

Not for most, but it is one option.

Read more...

more
2 0 350
DanPatterson
MVP Esteemed Contributor

This is a follow up to the Common points blog, but with floating point issues addressed because sometimes exact comparisons aren't great due to calculation artifacts or user input issues.

Read more...

more
3 0 489
DanPatterson
MVP Esteemed Contributor

A followup to 'Common segments', since it the points that count in the first place.  I will compare 'normal' to 'numpy' checks so you can add to your arsenal.

Read more...

more
1 1 554
DanPatterson
MVP Esteemed Contributor

Polygons have them.  Polylines sometimes.  It isn't just about matching coordinates.  It is about sharing commonalities.

Read more...

more
1 0 482
DanPatterson
MVP Esteemed Contributor

A combination of ideas.

Read more...

more
2 0 324
DanPatterson
MVP Esteemed Contributor

A point on line missive using numpy and a basic math reminder.

Read more...

more
8 0 600
Clubdebambos
MVP Regular Contributor

The spatial clause for a definition query arrived at ArcGIS Pro 3.5, and while an excellent addition to the definition query capabilities, it still comes up a little short by only enabling 'intersection' as the spatial relationship. In this blog post we will explore how you can generate your own ArcPy geometry object (polygon in this example) to overcome this limitation and apply a spatial clause to your definition query that suits your requirements. 

Read more...

more
4 2 696
Samy_BoumaNgock
Emerging Contributor

Hi everyone,

I’m sharing a working Python script that applies a UniqueValueRenderer symbology to a point feature class in ArcGIS Pro using arcpy. This implementation uses Picture Marker Symbols to represent each unique value in a field—specifically, country flags. The shared code is part of a broader implementation.

SamyBouma_Ngock_0-1748357457635.gif

Why This Matters

Many of us have faced challenges when working with CIM-based symbology in ArcGIS Pro. The documentation around the Cartographic Information Model (CIM) can be sparse or unclear, and issues often arise regardless of the programming language used.

This post aims to provide a working example that others can adapt and build upon.

 

Use Case

  • Feature class type: Point (e.g., country centroids)
  • Field used for symbology: Country_Flag
    • Must use the internal field name, not the alias
    • Field name is case-sensitive
  • Unique values: 238 (each representing a country)
  • Goal: Assign a Picture Marker Symbol (flag image) to each point based on the Country_Flag field

 

Implementation Steps

  • Set a default UniqueValueRenderer using a basic shape marker, keyed on the Country_Flag field.
  • Iterate through each unique value and assign a Picture Marker Symbol using images stored in a local folder.
    • Each image filename matches a value in the Country_Flag field exactly (e.g., France.png, Brazil.png).

 

Key Challenges

  • CIM complexity: The structure of CIM symbol layers is not always intuitive.
  • Field name sensitivity: Using the alias or incorrect casing can silently fail.
  • Lack of consistent examples: Many developers report inconsistent behavior or undocumented quirks when applying symbology programmatically.

 

Why I’m Posting

I’ve seen many threads where people are stuck on similar issues, so I wanted to contribute a working reference. If you're trying to automate symbology in ArcGIS Pro using arcpy, this might save you some time.

In the code below, read "data:image" in its HTML version data+colon symbol+image

Cheers,

    def set_defaultuniquevaluesrenderer(featurelayer=None, fieldname=None):
        """Set a default unique values renderer for the specified field in the feature layer."""
        if featurelayer is None:
            arcpy.AddError("Layer not found. Symbology not updated.")
        
        sym = featurelayer.symbology
        sym.updateRenderer('UniqueValueRenderer')
        sym.renderer.fields = [fieldname]
        featurelayer.symbology = sym
        arcpy.AddMessage(f"Symbology updated to UniqueValueRenderer on field: {fieldname}")

    def set_updatesymbology(featurelayer=None, fieldname=None, imagefolder=None):
        """Update only the point symbols as picture markers with corresponding images."""
        if featurelayer is None:
            arcpy.AddError("Layer not found. Symbology not updated.")

        # Get the CIM (Cartographic Information Model) definition of the layer
        cim = featurelayer.getDefinition('V3')
        renderer = cim.renderer

        # Iterate through each class in the renderer's first group
        for classDef in renderer.groups[0].classes:
            # Get the value for the symbol (usually the flag image filename)
            try:
                value = classDef.values[0].fieldValues[0]
            except (AttributeError, IndexError, TypeError):
                arcpy.AddWarning(f"Skipping class with no valid value for symbol: {classDef.name}")
                continue
            
            # Build the full path to the image file
            imagePath = os.path.join(imagefolder, value)
            if arcpy.Exists(imagePath):
                # Read and encode the image as base64
                with open(imagePath, "rb") as imgFile:
                    encoded = base64.b64encode(imgFile.read()).decode("utf-8")
                # Define a CIMPictureMarker using the encoded image
                pictureMarker = {
                    "type": "CIMPictureMarker",
                    "enable": True,
                    "size": 10,
                    "url": f"data:image/png;base64,{encoded}",
                }
                # Create a CIMPointSymbol with the picture marker
                pointSymbol = {
                    "type": "CIMPointSymbol",
                    "symbolLayers": [pictureMarker]
                }
                # Create a CIMSymbolReference for the class symbol
                symbolRef = {
                    "type": "CIMSymbolReference",
                    "symbol": pointSymbol
                }
                # Assign the new symbol to the class definition
                classDef.symbol = symbolRef

        # Apply the updated CIM definition back to the feature layer
        featurelayer.setDefinition(cim)
        arcpy.AddMessage("Point symbols updated with corresponding flag images.")

 

more
1 0 1,045
248 Subscribers
Labels