Hello,
I have followed How To: Append a New Row into a Feature Attribute Table from an Existing CSV File Using Py
It is working fine for the attribute table, which updates correctly, but the points on the map are not showing.
I created a simple example where I start with 5 points and then add 5 more, but after running the Python code, the map still displays only the initial 5 points. The attribute table of the feature class has all 10 points. Any Idea on how can I fix this?
You can force a map refresh by manually updating the CIM:
import arcpy
from arcpy._mp import Map
from arcpy.cim import CIMMap
prj = arcpy.mp.ArcGISProject(r'Path\to\your\project.aprx')
_map: Map = prj.activeMap
# or
_map = prj.listMaps()[0] # Set to map index (alphabetical order)
definition: CIMMap = _map.getDefinition('V3')
_map.setDefinition(definition)
This is basically what happens every time you pan or zoom in a map, but if you have a static view, you can force a refresh by updating the CIM definition (kinda like forcing a SQL trigger to run by setting `ID = ID`).
NOTE: You can remove the from imports and type hinting in your script, I just added them so you can use Pylance to see the structure of the objects returned by listMaps() and getDefinition() without having to dig through dozens of pages of documentation.
Also here's a reusable script that should get you on the right path to making this a generally applicable solution:
import arcpy
from arcpy._mp import Map
from arcpy.cim import CIMMap
def reload_map(_map: Map) -> None:
definition: CIMMap = _map.getDefinition('V3')
_map.setDefinition(definition)
def update_from_csv(csv_path: str, featureclass: str, fields: list[str], match_field: str, *,
suppress: bool=True, strict: bool=True) -> None:
# Build the update mapping
update_dict = {}
with arcpy.da.SearchCursor(csv_path, fields) as search:
for row in search:
dict_row = dict(zip(fields, row))
if dict_row[match_field] in update_dict and strict:
if not suppress:
raise ValueError('Duplicate match field value found')
else:
print(f"Duplicate match field value found: {dict_row[match_field]}, skipping")
continue
update_dict[dict_row[match_field]] = row
# Apply the update mapping
with arcpy.da.UpdateCursor(featureclass, fields) as update:
for row in update:
if row[match_field] in update_dict:
row = update_dict[row[match_field]]
update.updateRow(row)
elif not suppress:
print(f"Row not found for {row[match_field]}")
def main():
featureclass_path = r'C:\path\to\featureclass'
csv_path = r'C:\path\to\csv'
fields = ['field1', 'field2', 'field3']
match_field = 'field1'
project = arcpy.mp.ArcGISProject(r'path\to\project.aprx')
_map = project.listMaps()[0]
update_from_csv(csv_path, featureclass_path, fields, match_field)
reload_map(_map)
if __name__ == "__main__":
main()
Thank you for your reply! It was very helpful.
However, I am still not seeing the feature on the map. I tried your code, but it did not update either the map or the attribute table. I made a few modifications and was able to update the attribute table, but the map view remains unchanged.
Do you have any suggestions to help me resolve this issue?
I have attached my project along with the notebook.
import arcpy
import pandas as pd
import os
from arcpy.mp import ArcGISProject
# Define file paths using os - modify this to reflect your desired structure
base_directory = r"C:\Users\user\Documents\ArcGIS\Projects\UpdatePoints"
path_output = os.path.join(base_directory, "UpdatePoints.gdb") # Generalized path output
layer_points = "Earthquakes_points"
path_intable = os.path.join(base_directory, "earthquake_points.xls", "Earthquake_points$") # Generalized input table path
feature_path = os.path.join(path_output, layer_points) # Feature path from output
csv_path = os.path.join(base_directory, "earthquake_points_update.xls") # Generalized CSV path
# Get the current project and active map
project = arcpy.mp.ArcGISProject("CURRENT")
active_map = project.listMaps()[0] # Select the active map; change index if necessary
# Get the spatial reference from the active map
spatial_reference = active_map.spatialReference
# Convert the XY table to a point feature class
arcpy.management.XYTableToPoint(
in_table= path_intable,
out_feature_class=feature_path,
x_field="longitude",
y_field="latitude",
z_field=None,
coordinate_system='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]];-400 -400 1000000000;-100000 10000;-100000 10000;8.98315284119521E-09;0.001;0.001;IsHighPrecision'
)
def reload_map(map_object):
"""Reload the map to reflect the updated features."""
definition = map_object.getDefinition('V3')
map_object.setDefinition(definition)
print("Map reloaded.")
def update_from_csv(csv_path: str, featureclass: str, fields: list[str], match_field: str, *,
suppress: bool = True, strict: bool = True) -> None:
"""
Update features in a feature class from a CSV/XLSX file with checks for duplicates.
Args:
csv_path (str): The path to the CSV/XLSX file.
featureclass (str): The path to the feature class to update.
fields (list): List of field names to update.
match_field (str): The field to match records for updating.
suppress (bool): Suppress duplicate match warning.
strict (bool): Raise error on duplicate match field values.
"""
# Create a mapping dictionary for updates
update_dict = {}
#Iterate through the features using the UpdateCursor() function.
with arcpy.da.UpdateCursor(feature_path,copy_fields) as cursor:
for row in cursor:
key = row[0]
try:
new_row = csv_dict[key]
except KeyError:
print(f"Entry not found in csv: {copy_fields[0]} = {key}")
continue
cursor.updateRow(new_row)
del csv_dict[key]
with arcpy.da.InsertCursor(feature_path, copy_fields) as cursor:
for new_row in csv_dict.values():
cursor.insertRow(new_row)
print("Feature class updated successfully.")
# Read the Excel file and get the column names
dataframe = pd.read_excel(csv_path, sheet_name='Folha1') # Ensure this points to the right file
copy_fields = dataframe.columns.tolist() # Get all column names
#Create a dictionary to store the values for the fields.
csv_dict = {row[0]: row for row in arcpy.da.SearchCursor(csv_path, copy_fields)}
def main():
match_field = "id" # Field to use for matching records
# Update the feature class based on the CSV file
update_from_csv(csv_path, feature_path, copy_fields, match_field, suppress=True, strict=True)
# Get the current project and the active map
project = ArcGISProject("CURRENT")
map_object = project.listMaps()[0] # Get the first map in the project
# Reload the map to reflect the updates
reload_map(map_object)
if __name__ == "__main__":
main()
Okay, So I took a look, and it the map just needs to be closed and re-opened. I went ahead and wrote a python toolbox for you that should work:
import arcpy
from arcpy import Field
import arcpy.typing.describe as dtypes
from arcpy._mp import Map
class Toolbox:
def __init__(self):
self.label = "Update Features"
self.alias = "Update Features"
# List of tool classes associated with this toolbox
self.tools = [UpdateFeatures]
class UpdateFeatures:
def __init__(self):
self.label = "Update Features"
self.description = "Update Features"
self.canRunInBackground = False
def getParameterInfo(self):
in_features = arcpy.Parameter(
displayName = "Input Features",
name = "in_features",
datatype = "GPFeatureLayer",
parameterType = "Required",
direction = "Input")
match_field = arcpy.Parameter(
displayName = "Match Field",
name = "match_field",
datatype = "Field",
parameterType = "Required",
direction = "Input")
match_field.parameterDependencies = [in_features.name]
match_field.value = 'id'
update_features = arcpy.Parameter(
displayName = "Update Features",
name = "update_features",
datatype = "GPFeatureLayer",
parameterType = "Required",
direction = "Input")
insert_new_rows = arcpy.Parameter(
displayName = "Insert New Rows",
name = "insert_new_rows",
datatype = "GPBoolean",
parameterType = "Optional",
direction = "Input")
return [in_features, update_features, match_field, insert_new_rows]
def isLicensed(self):
return True
def updateParameters(self, parameters):
return
def updateMessages(self, parameters):
return
def execute(self, parameters, messages):
parameters: dict[str, arcpy.Parameter] = {param.name: param for param in parameters}
in_features: dtypes.Layer = arcpy.Describe(parameters['in_features'].value)
update_features: dtypes.Layer = arcpy.Describe(parameters['update_features'].value)
match_field: Field = parameters['match_field'].valueAsText
insert_new_rows: bool = parameters['insert_new_rows'].value
feature_count: int = int(arcpy.GetCount_management(in_features.catalogPath).getOutput(0))
# Build a set of fieldnames that are common to both input and update features
# We need to skip the OBJECT_ID field because it will change when we insert new rows
# Because later there is a field level check for changes, we need to exclude it here
in_fields: set = set(field.name for field in arcpy.ListFields(in_features.catalogPath) if 'OBJECTID' not in field.name)
update_fields: set = set(field.name for field in arcpy.ListFields(update_features.catalogPath) if 'OBJECTID' not in field.name)
# Compare the input and update features to ensure the match field exists in both
if match_field not in in_fields:
raise ValueError(f"Field '{match_field}' not found in input features")
if match_field not in update_fields:
raise ValueError(f"Field '{match_field}' not found in update features")
# Intersect the input and update fields to find the fields that are common to both
matching_fields: set = in_fields & update_fields
# Remove the match field from the set of matching fields so it can be added to the front of the cursor
matching_fields.remove(match_field)
# Create a list of fields to search on [ID, SHAPE, ...]
search_fields: list = [match_field, 'SHAPE@'] + list(matching_fields)
updates: dict = {
row[0]: row
for row in arcpy.da.SearchCursor(update_features.catalogPath, search_fields)
}
# Apply Updates
arcpy.SetProgressor("step", "Updating Features", 0, feature_count, 1)
with arcpy.da.UpdateCursor(in_features.catalogPath, search_fields) as cursor:
for row in cursor:
arcpy.SetProgressorPosition()
match_key = row[0]
# Only update if the match key is in the updates dictionary
if match_key not in updates:
continue
# Skip if the row is the same as the update (This method is required for SHAPE objects)
if all(a == b for a, b in zip(row, updates[match_key])):
updates.pop(match_key)
continue
# Update the row with the new values
cursor.updateRow(updates.pop(match_key))
arcpy.AddMessage(f"Updated feature with ID: {match_key}")
arcpy.ResetProgressor()
# Insert New Rows if requested
if insert_new_rows and updates:
arcpy.SetProgressor("step", "Inserting New Rows", 0, len(updates), 1)
with arcpy.da.InsertCursor(in_features.catalogPath, search_fields) as cursor:
for row in updates.values():
cursor.insertRow(row)
arcpy.AddMessage(f"Inserted new feature with ID: {row[0]}")
arcpy.SetProgressorPosition()
arcpy.ResetProgressor()
arcpy.AddMessage("Refreshing Map")
project = arcpy.mp.ArcGISProject("CURRENT")
active_map: Map = project.activeMap
project.closeViews()
active_map.openView()
return
def postExecute(self, parameters, messages):
return
You'll need to rename the file .pyt for it to load into arcpro, but it will close and re-open the map and allow for inclusive updating (only updates in the original table are applied) and merge updating (any records in the update features that are not in the target features will be added). It will also skip updating rows that havent changed and print out a message for each row that it does detect a change for.