Select to view content in your preferred language

Surround points with related points using Python

463
0
04-18-2022 09:39 AM
Labels (3)
RogerDunnGIS
Occasional Contributor II

I wrote some code in Python to modify a point feature class in a feature dataset which had an attribute relationship with another point feature class in the same dataset.  The problem domain was traffic signs on poles (or posts).  This is a one-to-many relationship.  If all the signs are at the exact location as the post, the signs become hard to distinguish from one another.

The following code spreads out the signs by a fixed number of feet, at equal intervals around the post.  Although the variables contain names like post and sign, one could modify the script to work with any two point feature classes that are related to each other.  The "children" will surround their "parents" at a fixed distance, spaced evenly around.

Point the code to your layers using the constants at the top.  Feel free to copy and paste parts of the code into Notebook cells to test parts of it at a time.  Modify the code below as you see fit, which may be necessary if I've made assumptions about how the offset algorithm works with your coordinate system.

Because the code modifies features, it is best practice to try this with a temporary copy of your dataset before proceeding to execute it on production data.  I make no guarantees as to the script's usefulness, suitability, or correctness in your work environment.

 

import arcpy
from arcgis import GeoAccessor
import pandas

# Set signWorkspace to where your feature classes are located.  If they
# are not in the same workspace, assign this to where the signs are located,
# as those will be the ones that are edited.
signWorkspace = r"Database Connections\Geodatabase.sde"
# Assign signWkid to the coordinate system of your signs feature class
signWkid = 3566
# Initialize constants related to poles/posts
postsLayer = "Traffic Sign Poles"
primaryKey = "GlobalID"
# OID@ is the same as OBJECTID, but more of a variable
lstPostFields = ["OID@", primaryKey]
postsQuery = "SHAPE IS NOT NULL"
# Initialize constants related to traffic signs
signsLayer = "Traffic Signs"
foreignKey = "POLE_ID"
lstSignFields = ["OID@", foreignKey, "GlobalID"]
signsQuery = f"SHAPE IS NOT NULL AND {foreignKey} IS NOT NULL"
# How far away from the parent to set the children around it.  The unit of
# measure will depend on the coordinate system of your features.
offset = 10

dfPosts = pandas.DataFrame.spatial.from_featureclass(postsLayer, fields = lstPostFields, where_clause = postsQuery)
dfPosts.set_index(primaryKey, inplace=True)

dfSigns = pandas.DataFrame.spatial.from_featureclass(signsLayer, fields = lstSignFields, where_clause = signsQuery)
dfSigns["SIGN_INDEX"] = -1
dfSigns["ALL_SIGNS"] = 1
dfSigns["ANGLE"] = 0
dfSigns["LAT"] = 0
dfSigns["LONG"] = 0

dfSignsOnPost = dfSigns.groupby(foreignKey).count()
dfSignsOnPost["INDICES_USED"] = 0

signObjId, signPoleId, signGuid, signShape, signIndex, signsAll, signAngle, signLat, signLong = dfSigns.columns

# For each sign...
for row in dfSigns.index:
    # Find the id of the related pole
    pole_id = dfSigns.at[row, signPoleId]
    # Find how many times we've encountered a sign on that post in this loop.
    # I call this the sign's "index"
    pole_used = dfSignsOnPost.at[pole_id, "INDICES_USED"]
    # Tell the sign how many signs are on that pole
    dfSigns.at[row, signsAll] = dfSignsOnPost.at[pole_id, "OID@"]
    # Assign the sign's index
    dfSigns.at[row, signIndex] = pole_used
    pole_used += 1
    # Tell the relationship how many signs have been found on that pole so far
    dfSignsOnPost.at[pole_id, "INDICES_USED"] = pole_used

# Join the signs to the posts they are on
dfSigns = dfSigns.join(dfPosts, foreignKey, rsuffix="POST")
postObjId, postShape = dfSigns.columns[-2:]

# Each sign gets its angle assigned based on its index and how many other
# signs are on the same pole.
dfSigns[signAngle] = 360 * dfSigns[signIndex] / dfSigns[signsAll]

dfSigns[signLong] = dfSigns.apply(lambda df: df[postShape]['x'] + offset * math.cos(math.radians(df[signAngle])), axis=1)
dfSigns[signLat] = dfSigns.apply(lambda df: df[postShape]['y'] + offset * math.sin(math.radians(df[signAngle])), axis=1)

dfPySigns = dfSigns.set_index(signGuid)
arcpy.env.workspace = signWorkspace
spatRef = arcpy.SpatialReference(signWkid)

editor = arcpy.da.Editor(arcpy.env.workspace)
editor.startEditing(False, True)
editor.startOperation()
try:
    with arcpy.da.UpdateCursor(signsLayer, lstSignFields + ["SHAPE@"], signsQuery) as uCursor:
        # For each sign...
        for row in uCursor:
            # Grab its global or unique ID
            guidVal = row[2]
            # Find its new lat/long
            newLat, newLong = dfPySigns.at[guidVal, signLat], dfPySigns.at[guidVal, signLong]
            # Create a point and assign it
            row[3] = arcpy.PointGeometry(arcpy.Point(newLong, newLat), spatRef)
            uCursor.updateRow(row)
    editor.stopOperation()
    editor.stopEditing(True)
except BaseException as err:
    print('Threw exception')
    editor.abortOperation()
    editor.stopEditing(False)
    raise
finally:
    del uCursor
del editor

 

Let me know if you liked this post with a Kudos.  Feel free to ask questions, too.

 

0 Replies