Select to view content in your preferred language

Updating map view after updating attribute table

452
3
12-16-2024 10:24 AM
JV_
by
Occasional Contributor

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?

0 Kudos
3 Replies
HaydenWelch
MVP Regular Contributor

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()

 

 

0 Kudos
JV_
by
Occasional Contributor

@HaydenWelch 

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()

 

 

HaydenWelch
MVP Regular Contributor

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.

 

0 Kudos