Python

Grouped Hole Structure

A grouped hole element stores a chain of closed elements. The first element is always a non-graphical complex header. Graphic elements are nested beneath that header. The outer shape (solid) and its associated inner shapes (holes) can be shape elements, ellipse elements, and/or complex shape elements that are in the same plane. Holes are not patterned and appear "transparent" in rendered views.

Grouped Hole Classes

I developed the grouped holes classes to extract data from a grouped hole element. They provide an API that is easy to use and reveal the essential data of a grouped hole.

The GroupedHoleData class is the top-level of a hierarchy. It contains a Python dictionary of AreaData instances that store data from each shape in the grouped hole.

The complete code of those classes and helper functions is available to download.

GroupedHoleData Class

The class constructors are assigned an ElementHandle, which is some kind of closed shape. They extract area data from that handle. A set of properties makes area and level data simple to reach.

class GroupedHoleData():
    '''
    Grouped hole data holds info harvested from a group hole...

    '''
    def __init__(self, eh: ElementHandle):
        self._id = eh.GetElementId()
        self._shapes = self.enumerate_shapes(eh)     # A dictionary of AreaData
        self._dgn_model = eh.GetDgnModel()

    def enumerate_shapes(self, header: ElementHandle)->dict:
        '''
        Enumerate the children of a grouped hole to record the inner (hole) elements.

        Returns: Dictionary of hole data.
        '''
        shapes = {}
        ExposeChildrenCount = ExposeChildrenReason(100)
        #ExposeChildrenQuery = ExposeChildrenReason(200)
        #ExposeChildrenEdit  = ExposeChildrenReason(300)
        component = ChildElemIter(header, ExposeChildrenCount)
        while component.IsValid():
            shapes[component.GetElementId()] = AreaData(component)
            component = component.ToNext()

        return shapes

    def has_level(self, level_id: int)->int:
        '''
        Returns the number of shapes on the specified level.
        '''
        count = 0
        for shape in self._shapes.values():
            if level_id == shape.level_id:
                count += 1
        return count

    @property
    def inner_shape_count(self)->int:
        '''
        Count the number of inner shapes (holes) in this grouped hole.
        The net number of shapes is, of course, one greater than this count.
        '''
        count = 0
        for shape in self._shapes.values():
            if shape.inner:
                count += 1
        return count

    @property
    def shapes(self)->dict:
        return self._shapes

    @property
    def id(self)->int:
        return self._id

    @property
    def dgn_model(self)->DgnModel:
        return self._dgn_model

    @property
    def area(self)->float:
        return self._shapes[self._id].area

    @property
    def perimeter(self)->float:
        return self._shapes[self._id].perimeter

    @property
    def outer(self)->bool:
        return self._shapes[self._id].outer

    @property
    def n_shapes(self)->int:
        return len(self._shapes)

    @property
    def levels(self)->list:
        '''
        Return a list of levels (level IDs) used by all shapes in this grouped hole.
        '''
        level_list = [int]
        for shape in self.shapes.values():
            level_list.append(shape.level_id)
        return level_list

    @property
    def net_area(self)->float:
        '''
        Calculate the net area of the grouped hole: the outer shape's area less the area of each inner shape.
        Inner shapes may have a negative area, and others a positive area, depending on how those shapes were created.
        Consequently, we take the absolute value of each shape's area, then negate it if it's a hole (inner shape).
        '''
        net = 0.0
        negate = 1.0
        for shape in self.shapes.values():
            if shape.outer:
                negate = 1.0
            else:
                negate = -1.0
            net += negate * shape.area
        return net

    def __str__(self):
        s = f"GroupedHole [{self.id}] N shapes={self.n_shapes} net area={format_area_value(self.net_area)}\n"
        for pos, shape in enumerate(self.shapes.values()):
            s += f"   [{pos + 1}] {shape}\n"
        return s

Area Data Class

The example project uses class AreaData to store information about inner (hole) and outer (solid) shapes harvested from a grouped hole …

class AreaData ():
    '''
    Area Data holds data harvested from shape and complex shape elements.

    '''
    def __init__(self, eh: ElementHandle):
        self._id = eh.GetElementId()
        #  Make the area positive, irrespective of the measured value
        self._area = abs(get_shape_area(eh))
        self._perim = get_shape_perimeter(eh)
        self._outer = IsSolid(eh)
        self._level_id = get_element_level(eh)

    @property
    def id(self)->int:
        return self._id

    @property
    def level_id(self)->int:
        return self._level_id

    @property
    def area(self)->float:
        return self._area

    @property
    def perimeter(self)->float:
        return self._perim

    @property
    def outer(self)->bool:
        return self._outer

    def __str__(self):
        if self.outer:
            return f"Solid [{self._id}] Perimeter {format_distance_value(self._perim)} Area {format_area_value(self._area)} level {self._level_id}"
        else:
            return f"Hole [{self._id}] Perimeter {format_distance_value(self._perim)} Area {format_area_value(self._area)} level {self._level_id}"

Helper Functions

Functions get_shape_area and get_shape_perimeter use the CurveVector API to mensurate the DGN elements …

get_shape_area function

def get_shape_area(eh: ElementHandle)->float:
    """
    Compute area of a planar closed CurveVector using CentroidAreaXY.
    Returns area (float) or None on failure.
    """
    curve_vector = get_shape_curvevector(eh)
    if curve_vector is None:
        return 0.0

    area = 0.0
    try:
        success, centroid, area = curve_vector.CentroidAreaXY()
    except Exception as e:
        print(f"Element [{eh.GetElementId()}] error computing area with CentroidAreaXY: {e}", file=sys.stderr)
        return 0.0

    if not success or area is None:
        print(f"Element [{eh.GetElementId()}] CentroidAreaXY failed to compute area.", file=sys.stderr)
        return 0.0

    return area

get_shape_perimeter function

def get_shape_perimeter(eh: ElementHandle)->float:
    """
    Compute perimeter (length) of a CurveVector.
    """
    perimeter = 0.0
    curve_vector = get_shape_curvevector(eh)
    if curve_vector is None:
        return 0.0
    try:
        perimeter = curve_vector.Length()
    except Exception as e:
        print(f"Element [{eh.GetElementId()}] error computing perimeter: {e}", file=sys.stderr)
        return 0.0
    return perimeter

get_shape_curvevector function

The function that converts an ElementHandle to a CurveVector is quite lengthy because it performs several tests to ensure that the result is valid …

def get_shape_curvevector(eh: ElementHandle)->CurveVector:
    """
    Returns CurveVector if it is a closed, planar region-like shape. Otherwise returns None.
    """
    curve_vector = get_curvevector(eh)
    if curve_vector is None:
        return None

    # Must be closed
    try:
        if not curve_vector.IsClosedPath():
            print(f"Element [{eh.GetElementId()}] is not a closed path", file=sys.stderr)
            return None
    except Exception as e:
        print(f"Element [{eh.GetElementId()}] error checking IsClosedPath: {e}", file=sys.stderr)
        return None

    # Must be planar
    try:
        localToWorld = Transform()
        worldToLocal = Transform()
        drange = DRange3d()
        is_planar = curve_vector.IsPlanar(localToWorld, worldToLocal, drange)
        if not is_planar:
            print(f"Element [{eh.GetElementId()}] is not planar", file=sys.stderr)
            return None
    except Exception as e:
        print(f"Element [{eh.GetElementId()}] error checking planarity: {e}", file=sys.stderr)
        return None

    return curve_vector

Formatting Measurements

Python, like the C++ MicroStationAPi and C# MicroStationNET, measure using MicroStation units-of-resolution (UORs). UORs are not human-friendly because they tend to be large numbers, several orders of magnitude bigger than measurements you might expect in feet, metres, square feet or metres². The Python API supplies formatters that convert UORs into human-friendly numbers

You can read more about the formatters.

The grouped holes example provides distance_formatter and area_formatter classes. They handle the shape perimeter and area values into something more reasonable than UORs.

Acknowledgements

Thanks go to MicroStation Programming Forum regulars …

They each provided help or suggestions that made my Python life easier.

Download Grouped Hole Example

Download la_solutions_find_grouped_holes.zip

Visit the Grouped Holes Download page.

Python Manager

Use MicroStation's Python Manager to find and execute the script.

Questions

Post questions about MicroStation programming to the MicroStation Programming Forum.