Select to view content in your preferred language

Issues with the arcpy.na.AddLocations utility.

178
6
Tuesday
gdesantis5
Occasional Contributor

Hello y'all,

Our dispatch center uses a CAD system that uses our GIS APIs to aid the dispatchers. One of the chief spatial questions that is automatically answers is what the closest available Fire unit and EMS unit to any given call is.

Recently, one of the municipalities within our county had a catastrophic hack. The hackers deleted all systems and data. Our county level dispatch center had to take all the calls. Reflecting on the incident, our communications directory over the dispatch operation, asked GIS to help solve the issue.

GIS is to create a product, that can be kept up to data, that can provide the following information for every address in the county, organized in a table, printed to a formatted PDF:

  • First Closest Fire Station
  • Second Closest Fire Station
  • Third Closest Fire Station
  • First Closest EMS Station
  • Second Closest EMS Station
  • Third Closest EMS Station
  • ESN Zone
  • Law Enforcement Zone

Before I move on to outlining the solution I am attempting to create, let me outline some of the moving elements the process will need to be resilient to. First, EMS and Fire stations can be moved, added or removed. While this is more involved for fire stations (it can still happen), it's a simple thing for the EMS stations. Some of these "EMS Stations" are not really stations but are concrete pads off a main road that an ambulance can idle on instead of driving constantly. Therefore, I cannot create the script thinking that the stations will remain consistent. Secondly, New addresses are added every day, old ones are deleted, and existing ones are moved. Thirdly, new streets are added every week. These may seem innocuous, but they have precluded me from attempting to solve this in simpler ways.

Now on to the current form of the solution. I have developed a python library that carries out this analysis using arcpy. Using varies loose terms and glossing over more details than I would like the process is as follows:

  1. Cleans and preps temporary workspace.
  2. Copies source data to a file geodatabase.
  3. Creates and validates a network dataset.
  4. Performs closest facility analysis for each address.
  5. Identifies containing zones for each address.
  6. Outputs results to Excel/PDF.

My current issue I am dealing with has to do with the attributes of the facility sublayer. I have attached a screenshot of one of them. Since the "Location OBJECTID #" mapping must be added separately, I was utilizing the append parameter of the arcpy.na.AddLocation utility to append the field mapping; filling out the Name, SourceID, and SourceOID fields with the appropriate values from the source layer so the results could be mapped back to the source values. It is from the source values that the excel/pdf is generated. However, I had to abandon this method due to the fact that the append parameter was only creating duplicate facilities as opposed to updating the existing ones. Now I am attempting to use arcpy.da.UpdateCursor and a dictionary to force the correct values in the table. But I am running into issues with that.

Do y'all have any insight into getting the append parameter to correctly function or how to get the update cursor to actually work.

 

6 Replies
TonyAlmeida
MVP Regular Contributor

For arcpy.na.AddLocations are you using append=Clear as a parameter?

Could you share your code?

What errors messages are you getting with the current code?

0 Kudos
gdesantis5
Occasional Contributor

Hello. For the first addLocation call I am using append=CLEAR [this is for the "Location OBJECTID #" mapping]. Then for the second addLocation call I am using append = APPEND[this is for the rest of the field mapping string].

0 Kudos
gdesantis5
Occasional Contributor

Here is a snippet of the code library that handles the addLocations bit.

TonyAlmeida
MVP Regular Contributor

I would check if null/ no match ID's

null_ids = 0
with arcpy.da.SearchCursor(facilities_layer, ["SourceID"]) as cursor:
    for row in cursor:
        if row[0] in (None, ""):
            null_ids += 1
logger.info(f"Found {null_ids} facilities with null SourceID")

 

Check field mapping

na_fields = [f.name for f in arcpy.ListFields(facilities_layer)]
logger.info(f"Available fields in NA layer: {na_fields}")
HaydenWelch
MVP Regular Contributor

Two things I noticed right off the bat:

1) You are trying to assign values to the row returned by the cursor which is an immutable tuple

with arcpy.da.UpdateCursor(facilities_layer, update_fields) as cursor:
    for row in cursor:
        ...
        if facility_data:
                    ...
                    row[1] = facility_data.get("FACILITYNA", "")
        ...

To get around this, you can cast the row to a list at the beginning of the cursor:

with arcpy.da.UpdateCursor(facilities_layer, update_fields) as cursor:
    for row in cursor:
        row = list(row)
        ...
        if facility_data:
                    ...
                    row[1] = facility_data.get("FACILITYNA", "")
        ...

or even better (assuming you know the field names and aren't relying on a structured record), you can make it a dictionary:

with arcpy.da.UpdateCursor(facilities_layer, update_fields) as cursor:
    for row in cursor:
        row_dict = dict(zip(cursor.fields, row))
        ...
        if facility_data:
                    ...
                    row_dict['NAME'] = facility_data.get("FACILITYNA", "")
        ...
        cursor.updateRow(tuple(row.values()))

Be careful with this one as you can't just pass row back to the update, you need to convert your dictionary values to a new row. This does require your dictionary to be ordered though, which is the default as of 3.7 and an official language feature (arcpy uses 3.12 now I believe? So you should be fine)

 

2) You are doing too much work inside the UpdateCursor

This is more of a code smell than anything. UpdateCursors maintain database locks, so you are tying up the target database with a cursor for the duration of your operation. Ideally you would build the update dictionary using a SearchCursor, then apply the updates all at once using UpdateCursor:

# Use a dictionary comprehension to map rows to an id key
records = {
     row[0] : row 
     for row in arcpy.da.SearchCursor(fc, fields, where_clause='<CLAUSE>', spatial_filter=Geometry())
}

# Initialize an update dict
updates: dict[int, tuple[Any, ...]] = {}

for rec_id, rec in records:
    # Do your row processing here
    updates[rec_id] = rec

with arcpy.da.UpdateCursor(fc, fields, where_clause='<CLAUSE>', spatial_filter=Geometry()) as cursor:
    for row in cursor:
        # use the index of your key field here, or use the dictionary trick I showed before
        rec = update.get(row[0])
        if rec:
            cursor.updateRow(rec)

In this example, the two cursors are only locking the database for as long as it takes to traverse them. All lengthy operations are now being done in Python/memory

 

I'm pretty sure that point one is the source of your cursor error though, you can pretty easily fix that by casting row to a list. I would definitely recommend you think about implementing some of the patterns in point 2 though. Debugging a big script like this that isn't typed and uses magic numbers for indexes can be an absolute pain. Especially when it's not failing, but giving incorrect/inconsistent results. 

gdesantis5
Occasional Contributor

Thank y'all,

I will need to put this down for a bit, but I will get back to it soon. At that time I will get responses to y'all.