ENV 859 - Advanced GIS

ArcPy: Writing a geoprocessing script - Part 2

ArcPy: Writing a geoprocessing script - Part 2Part I - RecapSetupPart II♦ Creating features from the observation track data5. Creating an empty feature class (to hold our observation points)5a. Learning the syntax for the ArcGIS Create Featureclass tool and adding it to our script5b. Adding a spatial reference parameter to the CreateFeatureclass tool. 5c. Enabling our script to overwrite outputs5d. Adding attributes to our newly created output feature classDebugging 😞6. Creating a point object from our locations data and adding it to the feature class6a. Creating the point object using our lat/long coordinatesβ—‹Debugging - again πŸ˜žβ†’ Python error messages...β†’ ArcGIS error messages via arcpy.GetMessages()β†’ Walking through codeβ†’ print() statements and examining variable values at various stages in our code6b. Inserting the point object and attribute values into our feature class♦ Cleanup & Final Touches7. Iterating through all ARGOS data files8. Linking our tool with ArcGISWhere to go from here?


Part I - Recap

Recall that our overall goal here is to extract meaningful data from the series of raw ARGOS data files and construct a useful ArcGIS Feature class of the data. Here is our intended workflow.

workflow

Up to this point, we've created code that reads in a single ARGOS data file, iterates through each location record, and parses out key information from that record including the record number, date, time, location class, and geographic coordinate.

Here, after taking a tour of the ArcPy module that we'll need to continue, we proceed with the remainder of our objectives:

Setup

To resume work, navigate to your ARGOSTracking workspace and open up your ImportARGOS.py script in PythonWin.

If needed, the code from where we are starting is found here.


Part II

♦ Creating features from the observation track data

From Task 2 in Part 1, we now have all the information we need to create a point feature and add it to a feature class, attributes and all. The two key scripting sub-tasks we need to do are (1) create an ArcPy point object from the coordinate information, and (2) add this to our output feature class as a new feature, accomplished using the ArcPy InsertCursor function. The ArcGIS help on the InsertCursor function has a short example from which we should be able to figure this out.

5. Creating an empty feature class (to hold our observation points)

5a. Learning the syntax for the ArcGIS Create Featureclass tool and adding it to our script

Before we can add any features to a feature class, however, we need to create the feature class. We'll use the ArcPy Create Featureclass tool to create the feature class, and then we'll use the AddField tool to add a few fields to hold attributes of our feature: the Tag ID, the date it was collected, and the Location Class (LC) of the ARGOS reading.

Consult the documentation on the Create Featureclass tool. You'll find that you have to supply the name of the feature class in two parts: a file path and a file name. So you'll have to add a bit of code to split your outputFC variable into its path and name components. The "os.path.split" function does this. (The os.path module is good for many file manipulation operations; consult the Python help documents for more information.)

β†’ Code link

 

5b. Adding a spatial reference parameter to the CreateFeatureclass tool.

The above code snippet includes only the required parameters of the CreateFeatureclass tool. It will default to a point feature class, with no "m" or "z" values, and have no defined spatial reference. Having no "m" or "z" values is fine, but we should address the fact that the spatial reference is undefined. Our options are: (1) leave it undefined; (2) hard-code it to be a specific spatial reference, or (3) allow the user to set it as an input variable. We'll choose the third option as it’s easy enough to do, and it makes our script that much more flexible.

To do this, however, we should add a new entry in our list of input variables. Later, we'll set this as an input parameter (sys.argv[3]), but for now we'll hard wire it to the "World Equidistance Cylindrical" projected coordinate system.

There's a catch here, however. Unlike setting a variable's value to a number or string, we're setting it to an ArcGIS Pro spatial reference object. So what exactly to we put on the right side of the = ?

To answer this, have a look a the ArcGIS Pro help on the spatial reference class. You'll see a few ways to create a spatial reference object linked to a specific coordinate system; we'll use the WKID option. You could look up the WKID associated with "World Equidistant Cylindrical" in the PDF ESRI provides, but I suggest a different way:

WKID

Now that we have the WKID, we can create the spatial reference variable:

β†’ Code link

5c. Enabling our script to overwrite outputs

We'll take care of this error, common when developing a script, by allowing ArcPy to overwrite output:

β†’ Code link

 

5d. Adding attributes to our newly created output feature class

When we create a new feature class (and don't specify a template) it won't include any additional attributes, so we'll have to add those programmatically. This is done with the AddField command, part of the management toolbox. We'll want to add the fields after we've created the feature class, but before we start iterating through the ARGOS data records.

β†’ Code link

 

Debugging 😞

Debugging, the process of finding and fixing errors, is a fact of [coding] life. Sometimes the error is easily spotted and fixed; sometimes not. Either way, it's best approached with a bit of sleuthing. Errors come in three flavors: syntax errors, coding errors, and logic errors:

β†’ [Fixed] Code link

 

6. Creating a point object from our locations data and adding it to the feature class

6a. Creating the point object using our lat/long coordinates

 

β—‹Debugging - again 😞

β†’ Python error messages...

As before, let's initiate our debugging effort by trying to narrow down where exactly the error occurs. First, the Python error message fails to reveal exactly which line broke our code, so we dig deeper....

β†’ ArcGIS error messages via arcpy.GetMessages()

If the error is linked to an ArcPy geoprocessing operation, an ArcGIS error is generated - the one that we'd see if we were running a tool in ArcGIS Pro . However, these messages are not displayed within Python; instead, we need to explicitly call the arcpy.GetMessages() command, which reveals all messages (status/error/warning) that would be displayed if the too were run within ArcGIS Pro:

The result is probably similar to:

which does not reveal any error here (just the status from successfully running the addField command). So the issue is not with an ArcPy processes, and we keep digging...

β†’ Walking through code

We could walk through each line of our code, but we'd' find that the error occurs somewhere in our loop: that would be a lot of walking...

β†’ print() statements and examining variable values at various stages in our code

However, we do have a print() statement in our loop, and we see it executes once before crashing. That tells us that it's crashing before it can fully process the second location record in our data. Let's test whether the bug is in lines 54-56, where we are trying to construct the point.

To do this, examine the value of the variables used in the commands. At the Python interactive prompt, type:

Aha! That's not a number - and the obsPoint.X = statement is expecting a number! It's a string and it can't even be converted to a number because of the W at the end. So, we need to add some code that strips off the "W" (or "E") from the longitude values and the "N" (or "S") from the latitude values. What more, however, is that we - knowing our geographic coordinate systems - need to set any coordinate west of the prime meridian or south of the equator to negative values.

More errors! But if you look at the output, we see that some Lat/Lon values are listed as "???????", which cannot be converted to numbers whatever we do. This is where we have to make a decision: do we comb through our input files and write code for every exception encountered, or do we accept what we should reasonably expect and just skip the rest, perhaps letting the user know any records that failed our criteria? In our case, we'll go with the latter, and we'll handle this with Python's try and except duo.

We handled the error well enough so that our script will run. However, we could have been much more thorough with reporting the error. For example, we could have added the script to tally how many errors occurred and provided the user some indication of how many records were skipped. Or, perhaps even more useful, we could have written a record of all TagIDs having errors to a log file so that the user could have some documentation of what records are missing from the output and why.

 

6b. Inserting the point object and attribute values into our feature class

So now our code can loop through all the records in the ARGOS data file, extract specific values to variables, and create an ArcPy point object. Next up, we need to add all that information into our output feature class as a feature and its attributes. Recall from our review of the ArcPy package that this is done with an insert cursor.

We'll add code to construct our insert cursor object outside of the loop reading all ARGOS records. Then, inside the loop, we'll add code that creates a new feature row object and subsequently assigns values to all the attributes, including the Shape@ attribute which will store the point feature created above:

Pause and appreciate all you've done so far. We've successfully created a feature class from coordinate data buried inside a raw data file. And even dealt with projections! A few notes before we move on:

β†’ First is that we are assuming all the ARGOS files are in the WGS84, but we could fairly easily redesign our script so that the inputSR variable become a user input - and place the onus on the script user to be sure the inputs are using the correct spatial reference.

β†’ Second is where our cur.InsertRow() statement occurs relative to the try: /exept: statements. Now that our script appears to be working smoothly, we should actually move (and indent) this statement inside the try: code block. This is actually quite important because, as is, if an error did occur in translating data to actual lat/long coordinates, the insertCursor() statement would still run, but with the previous record's coordinate values! Oops!

 

♦ Cleanup & Final Touches

7. Iterating through all ARGOS data files

Almost there! Our final task is to combine all we've done above, but instead of for a single ARGOS data file, do it for all files in a user specified folder. Like most scripting tasks, this can be done from several approaches. For example, we could create a new, second script that loops through each document in a folder and then calls the script we just completed. However, the most straightforward approach may simply be to adjust our current script a bit so that it: (1) Reads in a user folder rather than a user specified ARGOS data file; (2) Creates a list of ARGOS data files within that folder, and (3) Runs our existing code within a for loop. Let's take that approach.

 

8. Linking our tool with ArcGIS

Lastly, we'll remove our hard-coded inputs, replacing them with script inputs (using arcpy.GetParameterAsText()), and add some messages to be sent to the ArcGIS progress window.

 

Where to go from here?

Now that we have a working script, we can enhance our script with more functionality. I've provided a final script HERE that allows the user to specify a set of Location Classes that can be used to filter which ARGOS observations are included in the output. Turns out that all the records with improper coordinate information have a Location Class of "Z", so if we filter those out we really cut down on errors.

Some other enhancements we could potentially include would be to, instead of drawing observation points, actually draw a line connecting the tracks for a given tag ID. Or we could add some code that intelligently decides when to use the alternative coordinates within a track observation by measuring each point's distance from that of the previous observation point and use the nearest coordinate of the pair.

The options are limitless, but the moral of this story is that a scripting project can be much easier to accomplish when broken into simpler steps and build gradually. You should also have noted that keeping your script organized and well commented makes debugging, enhancing, and re-using scripts much easier. Finally, while scripting can have its frustrating moments, it has its rewards in seeing the computer do all your work for you and saving you gobs of time.