<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Surround points with related points using Python in ArcGIS Pro Questions</title>
    <link>https://community.esri.com/t5/arcgis-pro-questions/surround-points-with-related-points-using-python/m-p/1165205#M54151</link>
    <description>&lt;P&gt;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.&amp;nbsp; The problem domain was traffic signs on poles (or posts).&amp;nbsp; This is a one-to-many relationship.&amp;nbsp; If all the signs are at the exact location as the post, the signs become hard to distinguish from one another.&lt;/P&gt;&lt;P&gt;The following code spreads out the signs by a fixed number of feet, at equal intervals around the post.&amp;nbsp; 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.&amp;nbsp; The "children" will surround their "parents" at a fixed distance, spaced evenly around.&lt;/P&gt;&lt;P&gt;Point the code to your layers using the constants at the top.&amp;nbsp; Feel free to copy and paste parts of the code into Notebook cells to test parts of it at a time.&amp;nbsp; 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.&lt;/P&gt;&lt;P&gt;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.&amp;nbsp; I make no guarantees as to the script's usefulness, suitability, or correctness in your work environment.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;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&lt;/LI-CODE&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Let me know if you liked this post with a Kudos.&amp;nbsp; Feel free to ask questions, too.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
    <pubDate>Mon, 18 Apr 2022 16:39:01 GMT</pubDate>
    <dc:creator>RogerDunnGIS</dc:creator>
    <dc:date>2022-04-18T16:39:01Z</dc:date>
    <item>
      <title>Surround points with related points using Python</title>
      <link>https://community.esri.com/t5/arcgis-pro-questions/surround-points-with-related-points-using-python/m-p/1165205#M54151</link>
      <description>&lt;P&gt;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.&amp;nbsp; The problem domain was traffic signs on poles (or posts).&amp;nbsp; This is a one-to-many relationship.&amp;nbsp; If all the signs are at the exact location as the post, the signs become hard to distinguish from one another.&lt;/P&gt;&lt;P&gt;The following code spreads out the signs by a fixed number of feet, at equal intervals around the post.&amp;nbsp; 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.&amp;nbsp; The "children" will surround their "parents" at a fixed distance, spaced evenly around.&lt;/P&gt;&lt;P&gt;Point the code to your layers using the constants at the top.&amp;nbsp; Feel free to copy and paste parts of the code into Notebook cells to test parts of it at a time.&amp;nbsp; 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.&lt;/P&gt;&lt;P&gt;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.&amp;nbsp; I make no guarantees as to the script's usefulness, suitability, or correctness in your work environment.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;LI-CODE lang="python"&gt;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&lt;/LI-CODE&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;Let me know if you liked this post with a Kudos.&amp;nbsp; Feel free to ask questions, too.&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Mon, 18 Apr 2022 16:39:01 GMT</pubDate>
      <guid>https://community.esri.com/t5/arcgis-pro-questions/surround-points-with-related-points-using-python/m-p/1165205#M54151</guid>
      <dc:creator>RogerDunnGIS</dc:creator>
      <dc:date>2022-04-18T16:39:01Z</dc:date>
    </item>
  </channel>
</rss>

