|
BLOG
|
It is possible to create an ArcMap Python Addin tool that simultaneously captures clicked map coordinates stamped with a user name and date time in an ArcMap layer displayed on a map and collect them into a table in a Microsoft Access database housed on a shared network drive. The tool outlined in this blog only keeps track of the last coordinate clicked by each user in the Access coordinate table with the expectation that these records would be used like a clipboard by custom Access forms that control the creation or editing of records within Access. However, the tool code presented here could be modified to directly update any Access table from within ArcMap if prompts are added to the tool that let the user choose the Access records they want to create or edit as a result of each mouse click. The pictures below shows the result of using the tool to capture the location of two different STOP markings on a road. The table records in ArcMap and Access match completely after simply activating the tool and clicking on each marking. The tool only activates when the left mouse button is clicked within a map and the Alt, Ctrl and Shift keys are not being pressed. I created the Access tblGIS_COORDINATE table manually, but the ArcMap GIS_COORDINATE feature class will automatically be created by the tool if it doesn't exist in the default database and be added to the map as a layer and made visible if it is not currently a layer in the map or visible when the mouse is clicked. The tool ensures that the cache is cleared if the GIS_COORDINATE table view is open so that it will refresh to match the point location on the map and you may have to make Access the currently active program to see its table refresh if it is open. Technical Notes: To build this tool you should download the addin_assistant tool from https://www.arcgis.com/home/item.html?id=5f3aefe77f6b4f61ad3e4c62f30bff3b. If you are running Python 3 or higher you will have to fix one line of code in the makeaddin.py file created by the addin assistant by changing it from: print archive_file to print(archive_file) to build the tool use the wizard to first create a toolbar and then create a tool under it. For the tool icon I used the EditingRemoteEditSynchronize32.png file in the C:\Program Files (x86)\ArcGIS\Desktop10.6\bin\Icons directory, but you are welcome to design your own. . To install pyodbc you can do the following: pyodbc list here is a list of whl files. Download the right one for you. Open cmd as administrator and go to the directory containing the file. Run pip install pyodbc-xxxxx.whl . If you get an error about the odbcji32.dll required to use the {Microsoft Access Driver (*.mdb, *.accdb)} you can follow the instructions at Unable to load odbcji32.dll (MS Access ODBC driver) The tool captures coordinates in both the State Plane Spatial Reference preferred by my jurisdiction and in the GCS WGS 1984 Spatial Reference used by Google maps. You should change the State Plane Spatial Reference to your own preferred Spatial Reference. You can find the WKID of your preferred Spatial Reference by adding a layer that uses that Spatial Reference to your map and opening the Data Frame Properties dialog, choosing the Coordinate System tab and choosing the Spatial Reference under the Layers group at the bottom of the list. I am not an ArcGIS Pro user, so I have not explored whether or not a similar addin can be built for use in ArcGIS Pro, but the development of a tool for Pro shouldn't affect the pyodbc code that makes communication with Microsoft Access possible. I hope many of you will find this useful and intriguing. I am also open to suggestions for other ways that pyodbc and python code might be used to create tools that may integrate Access and ArcMap more fully. import arcpy
import pythonaddins
import getpass
from datetime import datetime
import pyodbc
class CaptureCoordinateTool(object):
"""Implementation for Capture_Coordinate2_addin.tool (Tool)"""
def __init__(self):
self.enabled = True
self.shape = "NONE" # Can set to "Line", "Circle" or "Rectangle" for interactive shape drawing and to activate the onLine/Polygon/Circle event sinks.
def onMouseDownMap(self, x, y, button, shift):
# Determine if the button and shift states are correct for activating the tool
if button != 1 or shift != 0: # The left mouse button was not pressed or the Alt, Ctrl and/or Shift keys were pressed
pass # Do default ArcMap behavior and exit
else: # The left mouse button was pressed and the Alt, Ctrl and Shift keys were not pressed
# Get information about the settings of the map clicked
mxd = arcpy.mapping.MapDocument("CURRENT") # get the current map where the mouse was pressed
df = arcpy.mapping.ListDataFrames(mxd)[0] # Assume the first data frame in the map was clicked
sr = df.spatialReference # get the spatial reference of the data frame
if sr.factoryCode != 2230 and sr.factoryCode != 4326: # Spatial Reference is invalid if it is not 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet) or 4326 (GCS_WGS_1984)
message = "Invalid Spatial Reference " + str(sr.factoryCode) + " - " + sr.name + " detected.\nPlease set the Spatial Reference to\n2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet)\nor 4326 (GSC_WGS_1984)"
pythonaddins.MessageBox(message, "Map Spatial Reference Is Invalid") # Display a message alerting user that the map has an invalid spatial reference and exit
else: # Spatial Reference is either 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet) or 4326 (GCS_WGS_1984) and therefore valid
try:
# Get the GIS_COORDINATE layer and if necessary Create the feature class or add the layer to the map
user_name = getpass.getuser() # get the login name of the user
out_path = "C:\Users\{}\Documents\ArcGIS\Default.gdb".format(user_name) # ArcMap Default geodartbase
out_name = "GIS_COORDINATE" # FC name
coordinate_fc = out_path + "\\" + out_name # Full path and name of FC
geometry_type = "POINT" # Features will be points
template = ""
has_m = "DISABLED"
has_z = "DISABLED"
spatial_ref = arcpy.SpatialReference(2230) # Set Spatial Reference to 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet) preferred by Riverside County
if not arcpy.Exists(coordinate_fc):
# Create an FC for displaying the clicked point in the preferred spatial reference and add fields to match the Access table schema
arcpy.CreateFeatureclass_management(out_path, out_name, geometry_type, template, has_m, has_z, spatial_ref)
arcpy.AddField_management(coordinate_fc, "USER_NAME", "TEXT", field_length=50)
arcpy.AddField_management(coordinate_fc, "LONGITUDE", "DOUBLE")
arcpy.AddField_management(coordinate_fc, "LATITUDE", "DOUBLE")
arcpy.AddField_management(coordinate_fc, "X_COORDINATE", "DOUBLE")
arcpy.AddField_management(coordinate_fc, "Y_COORDINATE", "DOUBLE")
arcpy.AddField_management(coordinate_fc, "COORDINATE_DATE", "DATE")
coordinate_lyr = None # Create an unassigned variable for a layer that will display the coordinate
for lyr in arcpy.mapping.ListLayers(mxd, "", df): # Check all existing layers in the dataframe clicked
if lyr.dataSource.lower() == coordinate_fc.lower(): # Check if any layer has the coordinate fc as its datasource
coordinate_lyr = lyr # set the coordinate layer variable to the map layer that has the coordinate feature class
coordinate_lyr.visible = True # make sure the layer is visible
if coordinate_lyr == None: # Check if no layer was found in the map
arcpy.MakeFeatureLayer_management(coordinate_fc, out_name) # Make a layer from the map, which should automatically be added to the map
for lyr in arcpy.mapping.ListLayers(mxd, "", df): # Recheck all existing layers in the dataframe clicked
if lyr.dataSource.lower() == coordinate_fc.lower(): # Find the created layer that has the coordinate fc as its datasource
coordinate_lyr = lyr # set the coordinate layer variable to the map layer that has the coordinate feature class
coordinate_lyr.visible = True # make sure the layer is visible
# Capture date for the point geometries and coordinates of both spatial references, the user name and the date time when the data was captured
x_stateplane, y_stateplane, longitude, latitude, state_plane_PtGeom, wgs_1984_PtGeom = 0, 0, 0, 0, None, None # Initialize variables for state plain and wgs 1984 coordinates and points
point = arcpy.Point() # Create a point object
point.X, point.Y = x, y # Set the x, y of the point object to the coordinates clicked
pointGeom = arcpy.PointGeometry(point, sr) # create a PointGeometry based on the point coordinates and spatial reference of the map click
if sr.factoryCode == 2230: # Spatial Reference is 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet)
state_plane_PtGeom = pointGeom # set state_plane_PtGeom to pointGeom clicked
x_stateplane, y_stateplane = x, y # x_stateplane and y_stateplane to x and y clicked
srOut = arcpy.SpatialReference(4326) # Create a 4326 (GCS_WGS_1984) Spatial Reference
wgs_1984_PtGeom = pointGeom.projectAs(srOut) # Project the point clicked to 4326 (GCS_WGS_1984) spatial reference
longitude, latitude = wgs_1984_PtGeom.centroid.X, wgs_1984_PtGeom.centroid.Y # set longitude and latitude to projected X and Y
elif sr.factoryCode == 4326: # Spatial Reference is 4326 (GCS_WGS_1984)
wgs_1984_PtGeom = pointGeom # set wgs_1984_PtGeom to pointGeom clicked
longitude, latitude = x, y # set longitude to latitude to x and y clicked
srOut = arcpy.SpatialReference(2230) # Create a 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet) Spatial Reference
state_plane_PtGeom = pointGeom.projectAs(srOut) # Project the point clicked to 2230 (NAD_1983_StatePlane_California_VI_FIPS_0406_Feet) spatial reference
x_stateplane, y_stateplane = state_plane_PtGeom.centroid.X, state_plane_PtGeom.centroid.Y # set x_stateplane and y_stateplane to projected X and Y
dtnow = datetime.now() # Capture the current datetime
# Cutput the captured data
conn_str = (
r'DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};'
r'DBQ=\\agency\agencydfs\Annex\Files\TRAFFIC\TRAFFICDB\TRAFFICDB\TRAFFICDB.accdb;'
) # set up a connection string for connecting to an Access accdb database
cnxn = pyodbc.connect(conn_str) # connect to the Access accdb database
crsr = cnxn.cursor() # create a cursor from the connection
crsr.execute("SELECT USER_NAME FROM tblGIS_COORDINATE WHERE USER_NAME = ?", user_name) # Execute a query to find any records that match the user_name that clicked the point
row = crsr.fetchone() # fetch the first record (there should only be none or one record)
if row: # If a row was found update the record with the coordinates and date time of the user click
crsr.execute("UPDATE tblGIS_COORDINATE SET LONGITUDE = ?, LATITUDE = ?, X_COORDINATE = ?, Y_COORDINATE = ?, COORDINATE_DATE = ? WHERE USER_NAME = ?", longitude, latitude, x_stateplane, y_stateplane, dtnow, user_name)
cnxn.commit()
else: # If no row was found insert a record for the current user_name with the coordinates and date time of the user click
crsr.execute("INSERT INTO tblGIS_COORDINATE(USER_NAME, LONGITUDE, LATITUDE, X_COORDINATE, Y_COORDINATE, COORDINATE_DATE) values (?, ?, ?, ?, ?, ?)", user_name, longitude, latitude, x_stateplane, y_stateplane, dtnow)
cnxn.commit()
crsr.close() # close the accdb cursor
cnxn.close() # close the accdb connection
arcpy.SelectLayerByAttribute_management(coordinate_lyr.name, "CLEAR_SELECTION") # Make sure no records are selected in the layer displaying the clicked coordinate
fields = ['SHAPE@', 'USER_NAME', 'LONGITUDE', 'LATITUDE', 'X_COORDINATE', 'Y_COORDINATE', 'COORDINATE_DATE'] # create a list of fields for the layer to be updated or inserted
count = 0 # Create a counter to determine if a record exists already or needs to be inserted
with arcpy.da.UpdateCursor(coordinate_lyr.name, fields) as cursor: # process an update cursor on the layer
for row in cursor: # iterate through all records
if row[1] == user_name: # only update a record if it matches the user_name of the the user that clicked the map
row[0] = state_plane_PtGeom # create the point shape using the point geomtery with preferred spatial reference
row[2] = longitude # update the longitude
row[3] = latitude # update the latitude
row[4] = x_stateplane # update the x coordinate in the preferred spatial reference
row[5] = y_stateplane # update the y coordinate in the preferred spatial reference
row[6] = dtnow # update the coordinatte date to the datetime of the map click
cursor.updateRow(row) # post the update to the row
count += 1 # increment the counter to so that another record will not be inserted
if count == 0: # determine if no feature with the user name exists
cursor = arcpy.da.InsertCursor(coordinate_lyr.name, fields) # if none exists create an insert cursor
cursor.insertRow((state_plane_PtGeom, user_name, longitude, latitude, x_stateplane, y_stateplane, dtnow)) # insert a new feature for the user with the coordinate and date time of the click
del cursor # delete the insert cursor
arcpy.RefreshActiveView() # refresh the active dataframe to show the point located at the position the user just clicked
except Exception as e: # catch any errors generated by the tool
pythonaddins.MessageBox("An Error Occurred Attempting to Capture a Coordinate\n" + str(e), "Error Capturing Coordinate") # Display a message showing that an error occurred executing the tool
... View more
01-09-2020
09:40 PM
|
1
|
2
|
1932
|
|
BLOG
|
I have not tried that. I am sure that this type of label would not work for generating a feature-linked annotation feature class, but it might work for a standard annotation feature class or an annotation group in a map document. All I could suggest is that you could try to create an annotation feature class or group from labels built using this technique to see if it generates an error. I would not be surprised if an error is produced, but since I have never tried it I can't say it would for sure.
... View more
01-02-2020
11:05 AM
|
0
|
0
|
16528
|
|
POST
|
Running the script within an editor session did not make a difference. The geomety vertices are not changing beyond positioning themselves relative to a new centroid. I found that I needed to change the field list for both the SearchCursor and the UpdateCursor to use the SHAPE@ field (the shape field geometry) instead of the SHAPE field (the field name, which apparently only affects the Centroid).
... View more
12-13-2019
04:41 PM
|
1
|
0
|
1840
|
|
POST
|
They are in the same Spatial Reference to strat with, which is why I tried it with and without the spatial reference parameter. The output feature class had representations in effect, so I tried exporting the features to eliminate the representations. However, I just noticed that ObjectID 11375 had 135 vertices in both before and after the update, while the source feature had only 56 vertices. So the original geometry appears to just have been moved to a new centroid, rather than updating the entire geometry. This occurred with or without the representations being in effect. Here are the vertices of the source feature. Here are the vertices of the output feature prior to update (with representations in effect). Here are the vertices of the output feature after the cursor updated it (without representations in effect). The geometry vertices did not change, only the location of the feature centroid. The script has not used an edit session to do the update. I will try doing the cursor update within an editor session to see if that affects the geometry vertices.
... View more
12-13-2019
04:06 PM
|
0
|
1
|
1840
|
|
POST
|
If I use a relate to select the features in the feature class I want to update and delete them, and then use the Append tool to insert the features from the source feature class, the geometry matches exactly. However, this changes the original ObjectIDs of the features in the updated feature class, which I wanted to avoid. The features shown below that are semi-transparent blue with red outlines have the source geometry I want to transfer and features that are orange with grey outlines are the features I want to change before I have done any update. The features shown below are the result of deleting the original features and using the Append tool. The geometry matches exactly, but the ObjectIDs have changed. I have written several versions of a script designed to preserve the ObjectIDs and transfer the geometry in the Shape field of one feature class using an da.UpdateCursor from the Shape field in another that match on the values of another field using both a Dictionary populated by a da.SearchCursor and directly from a da.SearchCursor, but the geometry that is transferred is not the same as the source geometry. I have tried variations of the script that used or did not use the Spatial Reference parameter of one or both cursors, but that does not fix the problem. The features shown below are the result of using a da.UpdateCursor to transfer the geometry. The ObjectIDs have not changed and the geometries have changed, but the transferred geometries are not identical to the source geometry. Has anyone else experienced this and come up with a solution that both preserves the original OBJECTIDs and correctly transfers geometry that is identical with the source geometry? Here is one of the scripts I tried. The script completes without error and the geometry of the updated feature class is being changed, but the output geometry is not identical to the source geometry as shown above. from time import strftime
print("Start script: " + strftime("%Y-%m-%d %H:%M:%S") )
import arcpy
desc = arcpy.Describe(r"Y:\GISData\rfairhur\Layers\Transform_Parcel_Layers\Transform_Parcel_Layers.gdb/ZONING_POST_DESERT_EXTRACT")
sourceFC = r"Y:\GISData\rfairhur\Layers\Transform_Parcel_Layers\Transform_Parcel_Layers.gdb/ZONING_POST_DESERT_EXTRACT"
sourceFieldsList = ['OIDLIST','SHAPE']
# Use list comprehension to build a dictionary from a da SearchCursor
valueDict = {r[0]:r[1] for r in arcpy.da.SearchCursor(sourceFC, sourceFieldsList, spatial_reference=desc.spatialReference)}
updateFC = r"Y:\GISData\rfairhur\Layers\Transform_Parcel_Layers\Transform_Parcel_Layers.gdb/ZONING_POST_DESERT_CURSOR"
updateFieldsList = ['OIDLIST','SHAPE']
count = 0
desc = arcpy.Describe(r"Y:\GISData\rfairhur\Layers\Transform_Parcel_Layers\Transform_Parcel_Layers.gdb/ZONING_POST_DESERT_CURSOR")
with arcpy.da.UpdateCursor(updateFC, updateFieldsList, spatial_reference=desc.spatialReference) as updateRows:
desc = arcpy.Describe(r"Y:\GISData\rfairhur\Layers\Transform_Parcel_Layers\Transform_Parcel_Layers.gdb/ZONING_POST_DESERT_EXTRACT")
for updateRow in updateRows:
# store the Join value of the row being updated in a keyValue variable
keyValue = updateRow[0]
# verify that the keyValue is in the Dictionary
if keyValue in valueDict:
expression = "OIDLIST = '" + keyValue + "'"
with arcpy.da.SearchCursor(sourceFC, sourceFieldsList, where_clause=expression, spatial_reference=desc.spatialReference) as sourceRows:
for sourceRow in sourceRows:
# transfer the value stored under the keyValue from the dictionary to the updated field.
updateRow[1] = sourceRow[1]
count += 1
updateRows.updateRow(updateRow)
if count % 50 == 0:
print("Fixed " +str(count) + " Zone Features: " + strftime("%Y-%m-%d %H:%M:%S") )
print("Fixed " +str(count) + " Zone Features: " + strftime("%Y-%m-%d %H:%M:%S") )
del valueDict
print( "Finished script: " + strftime("%Y-%m-%d %H:%M:%S") )
... View more
12-13-2019
02:39 PM
|
0
|
3
|
1907
|
|
BLOG
|
At the time I originally wrote this blog I had not really mastered the techniques I have described in my Turbo Charging Data Manipulation Using Python Cursors and Dictionaries. I have now written a script using that technique that can perform the steps described in the Creating Address Ranges on the Centerlines section of this blog without the need for creating any intermediate geoprocessiing output tables. The script is able to create ranges for all 29 of the jurisdictions in my County from over 200,000 address points for 50,000 centerline segments in under 5 minutes. I also wrote a script for creating the Locate Features Along Routes outputs for all of my jurisdictions and am currently working on scripts that can automate several aspects of the QC analysis processes described in blog. While I don't think I can completely eliminate the time someone would have to spend to manually correct data errors that impact the address range outputs, I can dramatically reduce the number of separate geoprocessing steps the user would perform to discover them and analyze them. I would like to hear from the users on this forum that are interested in my Address Range scripts before I publish a new blog on this topic, so please post a response to this blog if you are interested. I also plan on asking that anyone that uses or adapts the scripts I will be publishing in the new blog to retain a credit for my work in their copies of the scripts.
... View more
11-22-2019
08:48 PM
|
0
|
0
|
3765
|
|
POST
|
Executing 20,000 separate selections against 10 million records should take the amount of time you are seeing. This means you have to read through the 10 million records 20,000 times. You asked for my help based on my Turbo Charging Data Manipulation with Python Cursors and Dictionaries. That technique solves this problem by reading the 20,000 records into memory once and then running through each of the 10 million records once updating the record from a lookup dictionary. This eliminates 19,999 reads through the 10 million records. So instead of iterating the 20,000 records you need to do an update iteration through the 10 million records after loading the 20,000 records in the dictionary. I don't really know how to set up the update iteration of your 10 million records, so I can't suggest how to do that. However, reading the 20,000 records into a dictionary and iterating through a da.updateCursor would be done with this code: inputFC = arcpy.GetParameter(0)
dbName = arcpy.GetParameterAsText(1)
tblName = arcpy.GetParameterAsText(2)
fldStn = arcpy.GetParameterAsText(3)
fldX = arcpy.GetParameterAsText(4)
fldY = arcpy.GetParameterAsText(5)
# Check FC is point type, has ID field.
inDesc = arcpy.Describe(inputFC)# Get description of FC
if inDesc.shapeType.lower() != "point":
raise customErr, "Data must be point type"
if not inDesc.hasOID:
raise customErr, "Data must have an ID field"
# Get reference to active data frame
mxd = arcpy.mapping.MapDocument("CURRENT")
df = mxd.activeDataFrame
arcpy.AddWarning("Updating Database...")
cnGP = sqlite3.connect(dbName)
fields = ['Station_Value', 'SHAPE@X','SHAPE@Y']
valueDict = {r[0]:(r[1:]) for r in arcpy.da.SearchCursor(inputFC, fields)}
updateFC = dbName
updateFieldsList = [fldStn, fldX, fldY]
with arcpy.da.UpdateCursor(updateFC, updateFieldsList) as updateRows:
for updateRow in updateRows:
# store the Join value of the row being updated in a keyValue variable
keyValue = updateRow[0]
# verify that the keyValue is in the Dictionary
if keyValue in valueDict:
# transfer the values stored under the keyValue from the dictionary to the updated fields.
for n in range (1,len(sourceFieldsList)):
updateRow[n] = valueDict[keyValue][n-1]
updateRows.updateRow(updateRow)
del valueDict
... View more
10-31-2019
09:29 AM
|
2
|
0
|
2943
|
|
BLOG
|
Glad to hear it. You can experiment with the formatted label version now if you want.
... View more
10-28-2019
09:52 AM
|
0
|
0
|
16528
|
|
BLOG
|
Please show me the properties of the LINKID fields in both the feature class and the table. I want to confirm they are both Long fields. If one is Long and the other is Double, that would cause a problem, since Python would store the Long as 53861 and the double as 53861.0, and these two values do not match exactly. To make sure both are interpreted as Long values change Line 18 and Line 35 to: 18 relateKey = int(relateRow[0])
35 labelKey = int([LINKID])
... View more
10-28-2019
07:44 AM
|
0
|
0
|
16528
|
|
BLOG
|
How big are the Dictionary Record sets being reported for the different field lists you have tried? The record count of the dictionary should be less than the record count of the related table itself, because all records are being collapsed into just the count of unique LINKID values in your table. Probably the record count is higher when you use , because this would be summarized on the OBJECTID values, which are unique for every record, not the LINKIDs, which can be duplicated on multiple records. Perform a summary on your LINKID field of your related table to find out if the count of the dictionary matches the count or records summarized on the LINKID field. If they match the dictionary was loaded correctly. If the record count of the summary is less than the number of features in your feature class, than some features have no related records in the table, assuming that the LINKID values of the features are unique for each feature. Probably the label should avoid any special formatting or attempts to make it look like a table until you get a result. Try the simple label code below. # Initialize a global dictionary for a related feature class/table
relateDict = {}
def FindLabel ( [LINKID] ):
# declare the dictionary global so it can be built once and used for all labels
global relateDict
# only populate the dictionary if it has no keys
if len(relateDict) == 0:
# Provide the path to the relate feature class/table
relateFC = r"D:\delbay_v3_3.gdb\Envr_hab_ls_v3_3_delbay_sp_02"
# create a field list with the relate field first (ROUTE_NAME),
# followed by sort field(s) (COMNAME), then label field(s) (FEAT_LABEL)
relateFieldsList = ["LINKID", "COMNAME", "FEAT_LABEL"]
# process a da search cursor to transfer the data to the dictionary
with arcpy.da.SearchCursor(relateFC, relateFieldsList) as relateRows:
for relateRow in relateRows:
# store the key value in a variable so the relate value
# is only read from the row once, improving speed
relateKey = relateRow[0]
# if the relate key of the current row isn't found
# create the key and make it's value a list of a list of field values
if not relateKey in relateDict:
# [searchRow[1:]] is a list containing
# a list of the field values after the key.
relateDict[relateKey] = [relateRow[1:]]
else:
# if the relate key is already in the dictionary
# append the next list of field values to the
# existing list associated with the key
relateDict[relateKey].append(relateRow[1:])
# delete the cursor, and row to make sure all locks release
del relateRows, relateRow
# store the current label feature's relate key field value
# so that it is only read once, improving speed
labelKey = [LINKID]
# start building a label expression.
# My label has a bold key value header in a larger font
expression = '<FNT name="Arial" size="12"><BOL>{}</BOL></FNT>'.format(labelKey)
# determine if the label key is in the dictionary
if labelKey in relateDict:
# sort the list of the list of fields
sortedList = sorted(relateDict[labelKey])
# add a record count to the label header in bold regular font
expression += '\n<FNT name="Arial" size="10"><BOL>Species Count = {}</BOL></FNT>'.format(len(sortedList))
# process the sorted list
for fieldValues in sortedList:
# append related data to the label expression
# my label shows a list of related
# cross streets and measures sorted in driving order
expression += '\n{} - {}'.format(fieldValues[0], fieldValues[1])
# clean up the list variables after completing the for loop
del sortedList, fieldValues
else:
expression += '\n<FNT name="Arial" size="10"><BOL>Species Count = 0</BOL></FNT>'
expression += 'Not in {} dictionary IDs'.format(len(relateDict))
# return the label expression to display
return expression Please screen shot a record in the table view from a selected feature you are trying to label in your map view with the LINKID field value showing and also the selected set of records that match it in the related table's table view with the 3 field values showing.
... View more
10-25-2019
04:54 PM
|
0
|
0
|
16528
|
|
BLOG
|
Try the code below. I have adjusted the label expressions to fit your field values, which are both string, and to sort and print the Comname then the Feat_Label. The feature dataset of the layer should not matter. I also have changed the label for the case where no related record is found to report how many records are in the dictionary to help diagnose the problem. # Initialize a global dictionary for a related feature class/table
relateDict = {}
def FindLabel ( [LINKID] ):
# declare the dictionary global so it can be built once and used for all labels
global relateDict
# only populate the dictionary if it has no keys
if len(relateDict) == 0:
# Provide the path to the relate feature class/table
relateFC = r"D:\delbay_v3_3.gdb\Envr_hab_ls_v3_3_delbay_sp_02"
# create a field list with the relate field first (ROUTE_NAME),
# followed by sort field(s) (COMNAME), then label field(s) (FEAT_LABEL)
relateFieldsList = ["LINKID", "COMNAME", "FEAT_LABEL"]
# process a da search cursor to transfer the data to the dictionary
with arcpy.da.SearchCursor(relateFC, relateFieldsList) as relateRows:
for relateRow in relateRows:
# store the key value in a variable so the relate value
# is only read from the row once, improving speed
relateKey = relateRow[0]
# if the relate key of the current row isn't found
# create the key and make it's value a list of a list of field values
if not relateKey in relateDict:
# [searchRow[1:]] is a list containing
# a list of the field values after the key.
relateDict[relateKey] = [relateRow[1:]]
else:
# if the relate key is already in the dictionary
# append the next list of field values to the
# existing list associated with the key
relateDict[relateKey].append(relateRow[1:])
# delete the cursor, and row to make sure all locks release
del relateRows, relateRow
# store the current label feature's relate key field value
# so that it is only read once, improving speed
labelKey = [LINKID]
# variables to adjust table cell sizes
iMaxLbl1Sz = 0
iMaxLbl2Sz = 0
iSpace = 5
# determine if the label key is in the dictionary
if labelKey in relateDict:
# sort the list of the list of fields
sortedList = sorted(relateDict[labelKey])
# process the sorted list to determine cell spacing
for fieldValues in sortedList:
strLabel1 = fieldValues[0]
strLabel2 = fieldValues[1]
if (len(strLabel1) > iMaxLbl1Sz):
iMaxLbl1Sz = len(strLabel1)
if (len(strLabel2) > iMaxLbl2Sz):
iMaxLbl2Sz = len(strLabel2)
# clean up the fieldValues variable once the for loop is done
del fieldValues
# My label has a key value header followed by a record count
expression = labelKey
expression += '\n<UND>Species Count = {}</UND>'.format(len(sortedList)) + '_' * (iMaxLbl1Sz + iMaxLbl2Sz + iSpace + 1 - len('Species Count = {}'.format(len(sortedList))))
# process the sorted list
for fieldValues in sortedList:
strLabel1 = fieldValues[0]
strLabel2 = fieldValues[1]
k1 = (iMaxLbl1Sz - len(strLabel1)) + 2
k2 = iSpace + (iMaxLbl2Sz - len(strLabel2)) - 3
# append related data to the label expression
# my label shows a list of related
# cross streets and measures sorted in driving order
expression += '\n' + strLabel1 + "." * k1
expression += "|"
expression += "." * k2 + strLabel2 + "|"
# clean up all list variables after completing the for loops
del sortedList, fieldValues
else:
# My label has a key value header followed by a record count
expression = labelKey
expression += '\n<UND>Species Count = 0</UND>'
expression += 'Not in {} dictionary IDs'.format(len(relateDict))
# return the label expression to display
return expression
... View more
10-25-2019
09:59 AM
|
0
|
0
|
16528
|
|
BLOG
|
I can't interpret the code, since the indentation is gone. Please indent the code properly. I can say that line 47 and 60 need to change to just the field value, since your data in the field you are referencing is not a float value.
... View more
10-25-2019
09:36 AM
|
0
|
0
|
16528
|
|
BLOG
|
Jason: Can you please post your code. I may be able to see what modifications are causing the output to not count any features. It is critical that the LINKID field values in the related table and the field you used for the labeled features contain matching values. Using the wrong field from the feature class as your relate field that does not actually match any of the values in the LINKID field would likely not cause an error, but it would never find any related records in the related table and would produce labels without any related values listed and a related record count of 0.
... View more
10-24-2019
05:20 PM
|
0
|
0
|
16528
|
|
POST
|
I wanted to mention that the bearing conversion Python formula I gave works both to convert from Geographic bearings to Arithmetic bearings and vice versa. (%LOC_ANGLE% is the angle field or value you want to convert) (450 - %LOC_ANGLE%) % 360 Also, you can get the complimentary angle of any angle in either bearing system with the Python formula: (180 + %LOC_ANGLE%) % 360
... View more
09-18-2019
07:27 AM
|
0
|
0
|
1448
|
|
POST
|
When you use the Make Route Event Layer, you should actually not use the Offset field option (I mistakenly used it in the screenshot). You want the points to have no offset so that they will fall on the midpoint of the line directly. The offset_dist field was created to be used with the Bearing Distance to Line tool to create lines that extend from the centerline outward across the casing boundaries.
... View more
09-18-2019
07:17 AM
|
0
|
0
|
1448
|
| Title | Kudos | Posted |
|---|---|---|
| 1 | 03-24-2026 08:01 PM | |
| 6 | 02-23-2026 08:34 AM | |
| 1 | 03-31-2025 03:25 PM | |
| 1 | 03-28-2025 06:54 PM | |
| 1 | 03-16-2025 09:49 PM |
| Online Status |
Online
|
| Date Last Visited |
39m ago
|