Using Alignment.PointLocation() in Dynamo for Python script

Using Alignment.PointLocation() in Dynamo for Python script

cyberflow
Advisor Advisor
848 Views
5 Replies
Message 1 of 6

Using Alignment.PointLocation() in Dynamo for Python script

cyberflow
Advisor
Advisor

Hi all,

Right now i'm trying to use in a Python script on Dynamo the Alignment.PointLocation() method to retrieve the bearing of the alignement at a specific Structure.Station and Structure.Offset

(https://help.autodesk.com/view/CIV3D/2024/ENU/?guid=99d3b19c-21bd-ab19-6684-894892a4e34a)

Just want to be sure how to use and apply that method :
The Station, Offset, Tolerance, Easting, Northing, Bearing

The tolerance is the maximum range of station to look for according to the station entered ?

And the easting, northing and bearing are called into the arguments but ... that's just a place holder so the method can return those information in those specified variables in the arguments ?

TIA 

Frank Freitas

CAE/CAD/BIM Coordinator & Support Specialist

LinkedIn
0 Likes
Accepted solutions (1)
849 Views
5 Replies
Replies (5)
Message 2 of 6

cyberflow
Advisor
Advisor

Ahhhh jesus i do hate it when i spent a bunch of my time searching, decide to post and find the answer 5 minutes after : 

Here seems to be a nice example of the use of PointLocation : 
https://adndevblog.typepad.com/infrastructure/net/

I guess i need to us ref variables so that they have the scope to be used outside the method ?

Frank Freitas

CAE/CAD/BIM Coordinator & Support Specialist

LinkedIn
Message 3 of 6

cyberflow
Advisor
Advisor

Is it me or in Python the variable scope like in .Net isn't applicable ?

public void PointLocation(
	double station,
	double offset,
	double tolerance,
	ref double easting,
	ref double northing,
	ref double Bearing
)

 

No mather how hard i try to apply that method in Python for Dynamo; it doesn't change nothing to my variables : 

import clr
import time
import unicodedata  # For normalizing special characters

# Add references to AutoCAD and Civil 3D assemblies
clr.AddReference('AcMgd')
clr.AddReference('AcCoreMgd')
clr.AddReference('AcDbMgd')
clr.AddReference('AecBaseMgd')
clr.AddReference('AecPropDataMgd')
clr.AddReference('AeccDbMgd')

# Import necessary namespaces
import Autodesk
from Autodesk.AutoCAD.ApplicationServices import *
from Autodesk.AutoCAD.DatabaseServices import *
from Autodesk.Civil.ApplicationServices import *
from Autodesk.Civil.DatabaseServices import *
from Autodesk.Aec.DatabaseServices import *
import math
import System.Reflection

# Input from Dynamo
doc = IN[0]
global easting_out
global northing_out
global bearing

# Function to get bearing in radians
def get_bearing(pipe):
    start_point = pipe.StartPoint
    end_point = pipe.EndPoint
    dx = end_point.X - start_point.X
    dy = end_point.Y - start_point.Y
    return math.atan2(dy, dx)

# Function to convert radians to degrees
def rad_to_deg(radians):
    return radians * 180 / math.pi

# Function to normalize angle to [0, 360)
def normalize_angle(angle_deg):
    normalized = angle_deg - 360 * math.floor(angle_deg / 360.0)
    return normalized if normalized >= 0 else normalized + 360  # Ensure positive result

# Function to inspect object properties
def list_properties(obj):
    props = obj.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
    return [prop.Name for prop in props]

# Function to normalize description for consistent matching
def normalize_description(desc):
    if desc:
        # Normalize Unicode characters (e.g., "É" to "E") and convert to uppercase
        return unicodedata.normalize('NFKD', desc).encode('ASCII', 'ignore').decode('ASCII').upper()
    return ""

# Function to determine if structure is connected at the beginning or end of a pipe
def is_connected_at_start(structure, pipe):
    struct_loc = structure.Location
    dist_start = ((pipe.StartPoint.X - struct_loc.X)**2 + (pipe.StartPoint.Y - struct_loc.Y)**2)**0.5
    dist_end = ((pipe.EndPoint.X - struct_loc.X)**2 + (pipe.EndPoint.Y - struct_loc.Y)**2)**0.5
    return dist_start < dist_end  # True if closer to start, False if closer to end

# Function to extract the angle from COUDE description
def extract_coude_angle(desc):
    try:
        # Split the description (e.g., "COUDE 45") and get the angle part
        angle_str = desc.split()[-1]  # Last part should be the angle (e.g., "45")
        return float(angle_str)  # Convert to float (e.g., 45.0, 22.5)
    except (IndexError, ValueError) as e:
        # Default to 0 if parsing fails
        return 0.0

# Main processing
output = []

try:
    active_doc = Application.DocumentManager.MdiActiveDocument
    db = active_doc.Database
    civil_doc = CivilApplication.ActiveDocument
    network_ids = civil_doc.GetPipeNetworkIds()

    # Lock the document for writing
    doc_lock = None
    try:
        doc_lock = active_doc.LockDocument()
        output.append("Document locked for writing")

        # Use a single transaction for all networks
        with db.TransactionManager.StartTransaction() as t:
            for network_id in network_ids:
                try:
                    pipe_network = t.GetObject(network_id, OpenMode.ForRead)
                    structure_ids = pipe_network.GetStructureIds()
                    pipe_ids = pipe_network.GetPipeIds()

                    # Map structures to connected pipes
                    structure_to_pipes = {struct_id: [] for struct_id in structure_ids}
                    for pipe_id in pipe_ids:
                        pipe = t.GetObject(pipe_id, OpenMode.ForRead)
                        start_struct_id = pipe.StartStructureId
                        end_struct_id = pipe.EndStructureId
                        if not start_struct_id.IsNull and start_struct_id in structure_to_pipes:
                            structure_to_pipes[start_struct_id].append(pipe)
                        if not end_struct_id.IsNull and end_struct_id in structure_to_pipes:
                            structure_to_pipes[end_struct_id].append(pipe)

                    # Process each structure
                    for struct_id in structure_ids:
                        structure = t.GetObject(struct_id, OpenMode.ForRead)
                        output.append(f"Structure ID {struct_id} type: {structure.GetType().FullName}")
                        if not isinstance(structure, Autodesk.Civil.DatabaseServices.Structure):
                            output.append(f"Object with ID {struct_id} is not a Structure")
                            continue

                        output.append(f"Properties of Structure ID {struct_id}: {list_properties(structure)}")
                        raw_desc = structure.Description if structure.Description else ""
                        desc = normalize_description(raw_desc)  # Normalize and uppercase the description
                        output.append(f"Structure {struct_id} raw description: {raw_desc}")  # Log raw description
                        output.append(f"Structure {struct_id} normalized description: {desc}")  # Log normalized description
                        connected_pipes = structure_to_pipes.get(struct_id, [])
                        pipe_count = len(connected_pipes)
                        output.append(f"Structure {struct_id} has {pipe_count} connected pipes")

                        bearings = []
                        if pipe_count > 0:
                            for pipe in connected_pipes:
                                bearing = get_bearing(pipe)
                                bearings.append(bearing)
                                output.append(f"Pipe bearing for Structure {struct_id}: {rad_to_deg(bearing)} degrees")
                        else:
                            output.append(f"Structure {struct_id}, Warning: No connected pipes, rotation will be set to 0° if applicable")

                        new_rotation = None
                        if desc == "VANNE" and pipe_count == 2 and abs(bearings[0] - bearings[1]) < 0.01:
                            new_rotation = bearings[0]
                        elif desc == "VANNE" and pipe_count != 2:
                            new_rotation = 0.0  # Default rotation if pipe count is not 2
                            output.append(f"Structure {struct_id}, VANNE has {pipe_count} pipes (expected 2), setting rotation to 0°")
                        elif desc == "T" and pipe_count == 3:
                            if abs(bearings[0] - bearings[1]) < 0.01 and abs(bearings[0] - bearings[2]) > 0.01:
                                new_rotation = bearings[2]
                            elif abs(bearings[1] - bearings[2]) < 0.01 and abs(bearings[0] - bearings[1]) > 0.01:
                                new_rotation = bearings[0]
                            elif abs(bearings[0] - bearings[2]) < 0.01 and abs(bearings[1] - bearings[0]) > 0.01:
                                new_rotation = bearings[1]
                        elif desc == "T" and pipe_count != 3:
                            new_rotation = 0.0  # Default rotation if pipe count is not 3
                            output.append(f"Structure {struct_id}, T has {pipe_count} pipes (expected 3), setting rotation to 0°")
                        elif desc in ["BOUCHON", "REDUIT", "MANCHON"] and pipe_count in [1, 2]:
                            if pipe_count == 1:
                                pipe = connected_pipes[0]
                                is_start = is_connected_at_start(structure, pipe)
                                bearing = bearings[0]
                                if desc == "BOUCHON":
                                    new_rotation = bearing if is_start else bearing - math.pi  # -180° if at end
                                    output.append(f"Structure {struct_id}, BOUCHON connected at {'start' if is_start else 'end'}, setting rotation to {'bearing' if is_start else 'bearing - 180°':} {rad_to_deg(new_rotation)} degrees")
                                else:
                                    new_rotation = bearing  # Default for REDUIT/MANCHON
                            elif abs(bearings[0] - bearings[1]) < 0.01:
                                new_rotation = bearings[0]
                        elif desc in ["BOUCHON", "REDUIT", "MANCHON"] and pipe_count not in [1, 2]:
                            new_rotation = 0.0  # Default rotation if pipe count is not 1 or 2
                            output.append(f"Structure {struct_id}, {desc} has {pipe_count} pipes (expected 1 or 2), setting rotation to 0°")
                        elif desc == "POTEAU INCENDIE" and pipe_count == 1:
                            new_rotation = bearings[0]  # Align with the pipe bearing directly
                            output.append(f"Structure {struct_id}, POTEAU INCENDIE setting rotation to pipe bearing: {rad_to_deg(new_rotation)} degrees")
                        elif desc == "POTEAU INCENDIE" and pipe_count != 1:
                            new_rotation = 0.0  # Default rotation if pipe count is not 1
                            output.append(f"Structure {struct_id}, POTEAU INCENDIE has {pipe_count} pipes (expected 1), setting rotation to 0°")
                        elif desc in ["COUDE 11", "COUDE 22.5", "COUDE 45", "COUDE 90"] and pipe_count == 2:
                            pipe0, pipe1 = connected_pipes
                            struct_loc = structure.Location
                            dist_start0 = ((pipe0.StartPoint.X - struct_loc.X)**2 + (pipe0.StartPoint.Y - struct_loc.Y)**2)**0.5
                            dist_end0 = ((pipe0.EndPoint.X - struct_loc.X)**2 + (pipe0.EndPoint.Y - struct_loc.Y)**2)**0.5
                            pipe0_is_start = dist_start0 < dist_end0
                            dist_start1 = ((pipe1.StartPoint.X - struct_loc.X)**2 + (pipe1.StartPoint.Y - struct_loc.Y)**2)**0.5
                            dist_end1 = ((pipe1.EndPoint.X - struct_loc.X)**2 + (pipe1.EndPoint.Y - struct_loc.Y)**2)**0.5
                            pipe1_is_start = dist_start1 < dist_end1
                            incoming_bearing = bearings[0] if pipe0_is_start else bearings[1]
                            outgoing_bearing = bearings[1] if pipe0_is_start else bearings[0]

                            output.append(f"Structure {struct_id}, Incoming bearing (degrees): {rad_to_deg(incoming_bearing)}")
                            output.append(f"Structure {struct_id}, Outgoing bearing (degrees): {rad_to_deg(outgoing_bearing)}")

                            # Calculate the angle difference (normalized to [-180°, 180°])
                            angle_diff = normalize_angle(rad_to_deg(outgoing_bearing) - rad_to_deg(incoming_bearing))
                            if angle_diff > 180:
                                angle_diff -= 360  # Normalize to [-180°, 180°]
                            elif angle_diff < -180:
                                angle_diff += 360

                            # Check for near-straight pipes
                            coude_angle = extract_coude_angle(desc)  # Extract angle (e.g., 11, 22.5, 45, 90)
                            if abs(angle_diff) < 1.0:  # Near-straight (less than 1° difference)
                                new_rotation = incoming_bearing
                                output.append(f"Structure {struct_id}, {desc} pipes are near-straight (angle diff {angle_diff}°), setting rotation to incoming bearing: {rad_to_deg(new_rotation)} degrees")
                            else:
                                if angle_diff < 0:  # Outgoing turns left (counterclockwise)
                                    new_rotation = incoming_bearing + math.pi - math.radians(coude_angle)  # +180° - COUDE angle
                                    output.append(f"Structure {struct_id}, {desc} outgoing goes left, setting rotation to incoming + 180° - {coude_angle}°: {rad_to_deg(new_rotation)} degrees")
                                else:  # Outgoing turns right (clockwise)
                                    new_rotation = outgoing_bearing - math.radians(coude_angle)
                                    output.append(f"Structure {struct_id}, {desc} outgoing goes right, setting rotation to outgoing - {coude_angle}°: {rad_to_deg(new_rotation)} degrees")

                            # Force a 1° change if rotation matches current, with guard clause
                            current_rotation = structure.Rotation
                            current_deg = normalize_angle(rad_to_deg(current_rotation))
                            new_deg = normalize_angle(rad_to_deg(new_rotation))
                            if abs(current_deg - new_deg) < 1.0:  # Use degrees for comparison
                                new_rotation += math.radians(1)  # Add 1 degree
                                output.append(f"Structure {struct_id}, Forcing rotation change by +1° due to match with current rotation")
                        elif desc in ["COUDE 11", "COUDE 22.5", "COUDE 45", "COUDE 90"] and pipe_count != 2:
                            new_rotation = 0.0  # Default rotation if pipe count is not 2
                            output.append(f"Structure {struct_id}, {desc} has {pipe_count} pipes (expected 2), setting rotation to 0°")
                            
                        elif desc.upper().find("PUISARD") != -1:
                            output.append(f"Structure {struct_id} matched as PUISARD-type structure with description: {raw_desc}")
                            alignment_id = getattr(structure, 'RefAlignmentId', None)  # Get the alignment ID
                            output.append(f"Structure {struct_id}, RefAlignmentId: {alignment_id}")  # Debug alignment ID
                            if alignment_id and not alignment_id.IsNull:
                                alignment = t.GetObject(alignment_id, OpenMode.ForRead)
                                struct_loc = structure.Location  # Get the structure's 3D point (X, Y, Z)
                                easting = struct_loc.X  # Use X as Easting
                                northing = struct_loc.Y  # Use Y as Northing
                                tolerance = 10000.0  # Tolerance in drawing units (e.g., 5 meters or 5 feet)

                                try:
                                    # First, project the structure's coordinates onto the alignment to get station and offset
                                    station = structure.Station
                                    offset = structure.Offset                                    
                                    output.append(f"Structure {struct_id}, Projected Station: {station}, Offset: {offset}")

                                    # Now use the station and offset to get the point and bearing
                                    easting_out = 0.0
                                    northing_out = 0.0
                                    bearing = 0.0
                                    # easting_out, northing_out, bearing = alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing)
                                    # easting_out, northing_out, bearing = alignment.PointLocation(station, offset, tolerance)
                                    alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing)
                                    output.append(f"Structure {struct_id}, Computed Easting: {easting_out}, Northing: {northing_out}, Bearing: {rad_to_deg(bearing)} degrees")
                                    new_rotation = bearing  # Set rotation to the alignment's bearing (in radians)
                                except Exception as e:
                                    output.append(f"Structure {struct_id}, Error using PointLocation : {str(e)}")
                                    new_rotation = 0.0  # Fallback to 0° if projection fails
                                    output.append(f"Structure {struct_id}, Failed to project onto alignment, setting rotation to 0°")
                            else:
                                new_rotation = 0.0  # Fallback to 0° if no alignment
                                output.append(f"Structure {struct_id}, No valid alignment found, setting rotation to 0°")

                        # Apply rotation if calculated
                        if new_rotation is not None:
                            current_rotation = structure.Rotation
                            output.append(f"Structure {struct_id}, Current Rotation (degrees): {rad_to_deg(current_rotation)}")
                            # Normalize the new rotation for logging
                            normalized_new_rotation_deg = normalize_angle(rad_to_deg(new_rotation))
                            output.append(f"Structure {struct_id}, Description: {desc}, Setting Rotation (degrees): {normalized_new_rotation_deg}")

                            try:
                                structure.UpgradeOpen()
                                output.append(f"Applying rotation to Structure {struct_id}, WriteEnabled: {structure.IsWriteEnabled}")
                                structure.Rotation = new_rotation  # Explicitly set rotation
                                updated_rotation = structure.Rotation
                                output.append(f"Structure {struct_id}, Updated Rotation (degrees) after set: {rad_to_deg(updated_rotation)}")

                                # Attempt to force handle-grip update via PartData or BlockReference
                                if hasattr(structure, 'PartData'):
                                    part_data = structure.PartData
                                    output.append(f"Structure {struct_id}, PartData available: {part_data is not None}")
                                if hasattr(structure, 'BlockId') and not structure.BlockId.IsNull:
                                    block_ref = t.GetObject(structure.BlockId, OpenMode.ForWrite)
                                    output.append(f"Structure {struct_id}, BlockReference found, IsWriteEnabled: {block_ref.IsWriteEnabled}")
                                    if block_ref and isinstance(block_ref, BlockReference):
                                        output.append(f"Structure {struct_id} rotating BlockReference.Rotation to {rad_to_deg(new_rotation)}")
                                        block_ref.Rotation = new_rotation
                                        updated_block_rotation = block_ref.Rotation
                                        output.append(f"Structure {struct_id}, BlockReference Updated Rotation (degrees): {rad_to_deg(updated_block_rotation)}")
                                else:
                                    output.append(f"Structure {struct_id} has no valid BlockId, using Structure.Rotation only")

                            except Exception as upgrade_e:
                                output.append(f"Failed to upgrade or modify Structure {struct_id}: {str(upgrade_e)}")
                                raise

                    output.append(f"Finished processing network {network_id}")
                    active_doc.Editor.Regen()
                    active_doc.Editor.UpdateScreen()
                    time.sleep(1)  # Give Civil 3D time to update between networks

                except Exception as e:
                    output.append(f"Error for network {network_id}: {str(e)}")
                    raise

            output.append("Committing transaction for all networks")
            t.Commit()
            active_doc.Editor.Regen()
            active_doc.Editor.UpdateScreen()
            time.sleep(1)  # Final update

    finally:
        if doc_lock is not None:
            doc_lock.Dispose()
            output.append("Document lock disposed")

    active_doc.Database.CloseInput(True)
    active_doc.Editor.Regen()
    active_doc.Editor.UpdateScreen()
    time.sleep(1)  # Additional delay for display update

except Exception as e:
    output.append(f"Script failed: {str(e)}")

OUT = output

Frank Freitas

CAE/CAD/BIM Coordinator & Support Specialist

LinkedIn
0 Likes
Message 4 of 6

cyberflow
Advisor
Advisor
Accepted solution

I've finnaly found why I wasn't accessing properly the .PointLocation() method ...

It returns a list in tuple :

point_location_outputs = alignment.PointLocation(station, offset, tolerance, easting_out, northing_out, bearing_out)

The list looks like this :

point_location_outputs[0] = null
point_location_outputs[1] = easting
point_location_outputs[2] = northing
point_location_outputs[3] = bearing

Frank Freitas

CAE/CAD/BIM Coordinator & Support Specialist

LinkedIn
0 Likes
Message 5 of 6

hippe013
Advisor
Advisor

First off, I am by no means an expert in python. With that being said, I have not had any luck with the method that required the tolerance parameter. This is most likely because I am not using it correctly. Though it would seem to me that your tolerance, set at 10000, is too large.

 

Could I suggest you use this PointLocation method:

PointLocation(station As Double, offset As Double, ByRef easting As Double, ByRef northing As Double)

 

And then to get your bearing at that location, use the GetFirstDirivative method to get your bearing. 

GetFirstDerivative(point As Autodesk.AutoCAD.Geometry.Point3d) As Autodesk.AutoCAD.Geometry.Vector3d

 

This could then rule out issues with the tolerance value. That's my two cents. Hope it helps. 

Message 6 of 6

cyberflow
Advisor
Advisor

Thanx for the suggestion @hippe013 !

I actually made this work (Finally !) - Ill keep like this for now 

But your approach is really correct too

I only reduced the tolerance that was a big overkill before

Frank Freitas

CAE/CAD/BIM Coordinator & Support Specialist

LinkedIn
0 Likes