santa_cruz_precincts.py
1    # ----------------------------------------------------------------------
2    # santa_cruz_precincts.py
3    # Christopher Prendergast
4    # 2024/05/15
5    # ----------------------------------------------------------------------
6    #
7    import arcpy
8    import sys
9    from typing import List
10   from pathlib import Path
11   
12   global g_proj_dir
13   global g_in_gdb
14   global g_temp_gdb
15   global g_out_gdb
16   
17   
18   def full_path(root_dir: str, basename: str) -> str:
19       """ 
20       Convert basename to a full path given the root directory or 
21       geo-database path. 
22    
23       :param str root_dir:    The root directory or file geo-database 
24                               path. 
25       :param str basename:    The basename of a geo-database or feature 
26                               class. 
27       :return str:            The root and basename joined as a path. 
28       """
29       return str(Path(root_dir, basename))
30   
31   
32   def setup_env(proj_dir_str: str, in_gdb_str: str, temp_gdb_str: str,
33                 out_gdb_str: str) -> None:
34       """ 
35       Set up the geo-database environment. Assign values to global 
36       variables g_in_gdb and g_out_gdb. 
37    
38       :param str proj_dir_str:    The project directory. 
39       :param str in_gdb_str:      The basename of the input geo-database 
40                                   within the project directory. 
41       :param str temp_gdb_str:    The full path of the temporary 
42                                   geo-database. 
43       :param str out_gdb_str:     The basename of the output geo-database 
44                                   within the project directory. 
45       :return NoneType:           None 
46       """
47       #
48       # Allow overwriting outputs.
49       # Note: overwriting doesn't work if the layer is open in ArcGIS Pro
50       # due to lock. Closing ArgGIS Pro releases the lock and the outputs
51       # can be overwritten. See:
52       # https://community.esri.com/t5/python-questions/arcpy-env-overwriteoutput-true-fails/m-p/411113#M32410
53       #
54       arcpy.env.overwriteOutput = True
55       #
56       # Check the project directory exists.
57       #
58       global g_proj_dir
59       g_proj_dir = str(Path(proj_dir_str))
60       assert Path(g_proj_dir).is_dir(), \
61           f"Can't find the project directory {g_proj_dir}"
62       print("...project directory:", g_proj_dir)
63       #
64       # Assign global variables for the input and output geo-databases.
65       #
66       global g_in_gdb
67       g_in_gdb = full_path(proj_dir_str, in_gdb_str)
68       global g_temp_gdb
69       g_temp_gdb = temp_gdb_str
70       global g_out_gdb
71       g_out_gdb = full_path(proj_dir_str, out_gdb_str)
72       #
73       # Check the input and output geo-databases exist.
74       #
75       assert arcpy.Exists(g_in_gdb), \
76           f"Can't find input geo-database: {g_in_gdb}"
77       assert arcpy.Exists(g_temp_gdb), \
78           f"Can't find temporary geo-database: {g_temp_gdb}"
79       assert arcpy.Exists(g_out_gdb), \
80           f"Can't find output geo-database: {g_out_gdb}"
81       print("...input geo-database:", g_in_gdb)
82       print("...temporary geo-database:", g_temp_gdb)
83       print("...output geo_database:", g_out_gdb)
84   
85   
86   def load_shapefile(shape_file: str, out_gdb: str) -> str:
87       """ 
88       Load a shapefile into feature class with the same name in the 
89       specified geodatabase. 
90    
91       :param str shape_file:  The file path to the shapefile. 
92       :param str out_gdb:     The path to the geo-database. 
93       :return str:            The geo-database path to the feature class 
94                               created. 
95       """
96   
97       print("...load_shapefile, shape_file", shape_file)
98       print("...load_shapefile, out_gdb:", out_gdb)
99   
100      assert arcpy.Exists(shape_file), \
101          f"Can't find input shape_file: {shape_file}"
102      assert arcpy.Exists(out_gdb), \
103          f"Can't find input out_gdb: {out_gdb}"
104  
105      try:
106          result: arcpy.Result = (
107              arcpy.conversion.FeatureClassToGeodatabase(
108                  Input_Features=[shape_file],
109                  Output_Geodatabase=out_gdb
110              ))
111      except arcpy.ExecuteError:
112          #
113          # Handle geo-processing specific errors.
114          #
115          print("...load_shapefile, arcpy error executing "
116                "geo-processing tool.")
117          print(arcpy.GetMessages(2))
118          sys.exit(101)
119      except:
120          #
121          # Handle any other type of error.
122          #
123          e = sys.exc_info()[1]
124          print(e.args[0])
125          sys.exit(201)
126  
127      print(result.getMessages())
128      #
129      # Unpack first element of result object as return value.
130      #
131      ret_val = result.getOutput(0)
132      #
133      # This tool ony returns the path to the gdb not the feature class.
134      # So, construct path to feature class here.
135      #
136      ret_val = full_path(ret_val, str(Path(shape_file).stem))
137  
138      print("...load_shapefile,", arcpy.management.GetCount(ret_val),
139            "precincts loaded from shapefile.")
140      print("...load_shapefile, ret_val:", ret_val)
141      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
142      return ret_val
143  
144  
145  def load_table(dbf_file: str, out_gdb: str):
146      """ 
147      Load a dbf file into a table with the same name in the specified 
148      geo-database. 
149   
150      :param str dbf_file:    The file path to the dbf file. 
151      :param str out_gdb:     The path to the geo-database. 
152      :return str:            The geo-database path to the table created. 
153      """
154  
155      print("...load_table, dbf_file", dbf_file)
156      print("...load_table, out_gdb:", out_gdb)
157  
158      assert arcpy.Exists(dbf_file), \
159          f"Can't find input dbf_file: {dbf_file}"
160      assert arcpy.Exists(out_gdb), f"Can't find input out_gdb: {out_gdb}"
161  
162      try:
163          result: arcpy.Result = arcpy.conversion.TableToGeodatabase(
164              Input_Table=[dbf_file],
165              Output_Geodatabase=out_gdb)
166      except arcpy.ExecuteError:
167          #
168          # Handle geo-processing specific errors.
169          #
170          print("...load_table, arcpy error executing "
171                "geo-processing tool.")
172          print(arcpy.GetMessages(2))
173          sys.exit(102)
174      except:
175          #
176          # Handle any other type of error.
177          #
178          e = sys.exc_info()[1]
179          print(e.args[0])
180          sys.exit(202)
181  
182      print(result.getMessages())
183      #
184      # Unpack first element of result object as return value.
185      #
186      ret_val = result.getOutput(0)
187      #
188      # This tool ony returns the path to the gdb not the feature class.
189      # So, construct path to feature class here.
190      #
191      ret_val = full_path(ret_val, str(Path(dbf_file).stem))
192      print("...load_table,", arcpy.management.GetCount(ret_val),
193            "cross_reference records loaded from dbf file.")
194      print("...load_table, ret_val:", ret_val)
195      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
196      return ret_val
197  
198  
199  def create_table(out_path: str, out_name: str) -> str:
200      """ 
201      Create a new empty table in the specified geo-database. 
202   
203      :param str out_path:    The path to the geo-database 
204      :param str out_name:    The name of the table to create. 
205      :return str:            The geo-database path to the table created. 
206      """
207  
208      print("...create_table, out_path", out_path)
209      print("...create_table, out_name:", out_name)
210  
211      assert arcpy.Exists(out_path), \
212          f"Can't find input out_path: {out_path}"
213  
214      try:
215          result: arcpy.Result = arcpy.management.CreateTable(
216              out_path=out_path,
217              out_name=out_name
218          )
219      except arcpy.ExecuteError:
220          #
221          # Handle geo-processing specific errors.
222          #
223          print("...create_table, arcpy error executing "
224                "geo-processing tool.")
225          print(arcpy.GetMessages(2))
226          sys.exit(103)
227      except:
228          #
229          # Handle any other type of error.
230          #
231          e = sys.exc_info()[1]
232          print(e.args[0])
233          sys.exit(203)
234  
235      print(result.getMessages())
236      #
237      # Unpack first element of result object as return value.
238      #
239      ret_val = result.getOutput(0)
240      print("...create_table, ret_val:", ret_val)
241      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
242      return ret_val
243  
244  
245  def add_fields(in_table: str,
246                 field_description: List[List[str]]) -> str:
247      """ 
248      Add fields to a table in the geo-database. 
249   
250      :param str in_table:            The geo-database path to the table. 
251      :param str field_description:   Specification of fields to add. 
252      :return str:                    The geo-database path to the table. 
253      """
254  
255      print("...add_fields, in_table", in_table)
256      print("...add_fields, field_description:", field_description)
257  
258      assert arcpy.Exists(in_table), \
259          f"Can't find input in_table: {in_table}"
260  
261      try:
262          result: arcpy.Result = arcpy.management.AddFields(
263              in_table=in_table,
264              field_description=field_description)
265      except arcpy.ExecuteError:
266          #
267          # Handle geo-processing specific errors.
268          #
269          print("...add_fields, arcpy error executing "
270                "geo-processing tool.")
271          print(arcpy.GetMessages(2))
272          sys.exit(104)
273      except:
274          #
275          # Handle any other type of error.
276          #
277          e = sys.exc_info()[1]
278          print(e.args[0])
279          sys.exit(204)
280  
281      print(result.getMessages())
282      #
283      # Unpack first element of result object as return value.
284      #
285      ret_val = result.getOutput(0)
286      print("...add_fields, ret_val:", ret_val)
287      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
288      return ret_val
289  
290  
291  def join_by_field(in_data: str, in_field: str, join_table: str,
292                    join_field: str, fields: List[str],
293                    index_join_fields="NEW_INDEXES") -> str:
294      """ 
295      Join two feature classes/tables based on a common key and transfer 
296      the specified fields to one of the tables. 
297   
298      :param str in_data:         The geo-database path to the 1st table. 
299      :param str in_field:        The join field in the 1st table. 
300      :param str join_table:      The geo-database path to the 2nd table. 
301      :param str str join_field:  The join field in the 2nd table. 
302      :param list fields:         The list of fields to transfer. 
303      :param str index_join_fields:   Whether to create new indexes. 
304      :return str:                The geo-database path to the resulting 
305                                  table. 
306      """
307  
308      print("...join_field, in_data", in_data)
309      print("...join_field, in_field:", in_field)
310      print("...join_field, join_table:", join_table)
311      print("...join_field, fields:", fields)
312      print("...join_field, index_join_fields:", index_join_fields)
313  
314      assert arcpy.Exists(in_data), f"Can't find input in_data: {in_data}"
315      assert arcpy.Exists(join_table), \
316          f"Can't find input in_table: {join_table}"
317  
318      try:
319          result: arcpy.Result = arcpy.management.JoinField(
320              in_data=in_data,
321              in_field=in_field,
322              join_table=join_table,
323              join_field=join_field,
324              fields=fields,
325              index_join_fields=index_join_fields
326          )
327      except arcpy.ExecuteError:
328          #
329          # Handle geo-processing specific errors.
330          #
331          print("...join_by_field, arcpy error executing "
332                "geo-processing tool.")
333          print(arcpy.GetMessages(2))
334          sys.exit(105)
335      except:
336          #
337          # Handle any other type of error.
338          #
339          e = sys.exc_info()[1]
340          print(e.args[0])
341          sys.exit(205)
342  
343      print(result.getMessages())
344      #
345      # Unpack first element of result object as return value.
346      #
347      ret_val = result.getOutput(0)
348      print("...join_field,", arcpy.management.GetCount(ret_val),
349            "records after join.")
350      print("...join_field, ret_val:", ret_val)
351      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
352      return ret_val
353  
354  
355  def dissolve(in_features: str, out_feature_class: str,
356               dissolve_field: str) -> str:
357      """ 
358      Dissolve the polygons in a feature class based on a shared value in 
359      :a field. 
360   
361      :param str in_features:       The path to the input feature class. 
362      :param str out_feature_class: The path to the output feature class. 
363      :param str dissolve_field:    The field to dissolve on. 
364      :return str:                  The path to the output feature class. 
365      """
366  
367      print("...dissolve, in_features", in_features)
368      print("...dissolve, out_feature_class:", out_feature_class)
369      print("...dissolve, dissolve_field:", dissolve_field)
370  
371      assert arcpy.Exists(in_features), \
372          f"Can't find input in_features: {in_features}"
373  
374      try:
375          result: arcpy.Result = arcpy.analysis.PairwiseDissolve(
376              in_features=in_features,
377              out_feature_class=out_feature_class,
378              dissolve_field=[dissolve_field]
379          )
380      except arcpy.ExecuteError:
381          #
382          # Handle geo-processing specific errors.
383          #
384          print("...dissolve, arcpy error executing "
385                "geo-processing tool.")
386          print(arcpy.GetMessages(2))
387          sys.exit(106)
388      except:
389          #
390          # Handle any other type of error.
391          #
392          e = sys.exc_info()[1]
393          print(e.args[0])
394          sys.exit(206)
395  
396      print(result.getMessages())
397      #
398      # Unpack first element of result object as return value.
399      #
400      ret_val = result.getOutput(0)
401      print("...dissolve,", arcpy.management.GetCount(ret_val),
402            "records after dissolve.")
403      print("...dissolve, ret_val:", ret_val)
404      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
405      return ret_val
406  
407  
408  def explode_xref(xref_table: str, xref_explode_table: str) -> str:
409      """ 
410      Explode any regular precincts for each voting precinct in 
411      the cross-reference table into multiple separate rows in a new 
412      table. Note that the key and value in the exploded results table is  
413      swapped from the input cross-reference table. The key in the output 
414      table is the regular precinct and there can be only one voting 
415      precinct for each regular precinct in the output. Each voting 
416      precinct can appear in multiple rows in the output. 
417   
418      :param str xref_table:      The path to the cross-reference table. 
419      :param str xref_explode_table:  The path to the exploded table. 
420      :return str:                The path to the exploded table. 
421      """
422  
423      print("...explode_xref, xref_table", xref_table)
424      print("...explode_xref, xref_explode_table:", xref_explode_table)
425  
426      assert arcpy.Exists(xref_table), \
427          f"Can't find input xref_table: {xref_table}"
428      assert arcpy.Exists(xref_explode_table), \
429          f"Can't find input xref_explode_table: {xref_explode_table}"
430  
431      #
432      # Iterate over the cross-reference table and unpack multiple regular
433      # precincts dictionary entries. The dictionary key is the regular 
434      # precinct and the value is the associated voting precinct.
435      #
436      precincts_1to1 = {}
437      try:
438          with (arcpy.da.SearchCursor(
439                  xref_table, ["VotePrec", "_Precincts"]) as search_cur):
440              for voting_precinct, regular_precincts_str in search_cur:
441                  #
442                  # Unpack the regular precincts into individual items and
443                  # add voting precinct to a dictionary with regular
444                  # precinct as the key. Note: using a set here just in
445                  # case there are any duplicate regular precincts in the
446                  # same cross-reference row.
447                  #
448                  regular_precincts_set = \
449                      set(regular_precincts_str.split())
450                  for regular_precinct in regular_precincts_set:
451                      precincts_1to1[regular_precinct] = \
452                          voting_precinct
453      except arcpy.ExecuteError:
454          #
455          # Handle geo-processing specific errors.
456          #
457          print("...explode_xref, arcpy error executing "
458                "geo-processing tool.")
459          print("...explode_xref, error in SearchCursor")
460          print(arcpy.GetMessages(2))
461          sys.exit(107)
462      except:
463          #
464          # Handle any other type of error.
465          #
466          e = sys.exc_info()[1]
467          print(e.args[0])
468          sys.exit(207)
469      # 
470      # Insert the contents of the dictionary into a table so that we can
471      # use geo-processing tools to join to this table.
472      #
473      try:
474          with (
475              arcpy.da.InsertCursor(
476                  xref_explode_table,
477                  ["Precinct", "VotePrec"]) as insert_cur):
478              for regular_precinct, voting_precinct \
479                      in precincts_1to1.items():
480                  insert_cur.insertRow(
481                      [regular_precinct,
482                       precincts_1to1[voting_precinct]
483                       ]
484                  )
485      except arcpy.ExecuteError:
486          #
487          # Handle geo-processing specific errors.
488          #
489          print("...explode_xref, arcpy error executing "
490                "geo-processing tool.")
491          print("...explode_xref, error in InsertCursor")
492          print(arcpy.GetMessages(2))
493          sys.exit(108)
494      except:
495          #
496          # Handle any other type of error.
497          #
498          e = sys.exc_info()[1]
499          print(e.args[0])
500          sys.exit(208)
501      ret_val = xref_explode_table
502      print("...explode_xref, ret_val:", ret_val)
503      print("...explode_xref,", arcpy.management.GetCount(ret_val),
504            "regular precincts have a voting precinct assigned.")
505      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
506      return ret_val
507  
508  
509  def update_precincts(precincts_fc: str) -> str:
510      """ 
511      Update the precincts feature class so that regular precincts that 
512      don't appear in the cross-reference table have their voting precinct 
513      set to the same value as their regular precinct. This is necessary 
514      to avoid all these precincts being dissolved into a single polygon 
515      because the share the same null value for voting precinct. 
516   
517      :param str precincts_fc: The path to the precincts feature class. 
518      :return str:             The path to the precincts feature class. 
519      """
520  
521      print("...update_precincts, precincts_fc", precincts_fc)
522  
523      assert arcpy.Exists(precincts_fc), \
524          f"Can't find input precincts_fc: {precincts_fc}"
525  
526      try:
527          with arcpy.da.UpdateCursor(
528                  precincts_fc, ["Precinct", "VotePrec"],
529                  where_clause='"VotePrec" is null') as update_cur:
530              update_count = 0
531              for row in update_cur:
532                  precinct, voting_precinct = row
533                  if voting_precinct is None:
534                      row[1] = precinct
535                      update_cur.updateRow(row)
536                      update_count += 1
537              print("...update_precincts,", update_count, "rows updated.")
538      except arcpy.ExecuteError:
539          #
540          # Handle geo-processing specific errors.
541          #
542          print("...update_precincts, arcpy error executing "
543                "geo-processing tool.")
544          print("...update_precincts, error in UpdateCursor")
545          print(arcpy.GetMessages(2))
546          sys.exit(109)
547      except:
548          #
549          # Handle any other type of error.
550          #
551          e = sys.exc_info()[1]
552          print(e.args[0])
553          sys.exit(209)
554  
555      ret_val = precincts_fc
556      print("...update_precincts, ret_val:", ret_val)
557      print(
558          "...update_precincts,",
559          arcpy.management.GetCount(ret_val),
560          "regular precincts with a voting precinct assigned after update."
561      )
562      assert arcpy.Exists(ret_val), f"Can't find feature class: {ret_val}"
563      return ret_val
564  
565  
566  def run_tools() -> None:
567      """ 
568      Run the geo-processing tools to buffer and join the various feature 
569      classes. 
570   
571      :return NoneType: None 
572      """
573      #
574      # Load precincts from shapefile.
575      #
576      precincts_shp_file = str(
577          Path(
578              g_proj_dir,
579              "Precincts",
580              "Precincts.shp"
581          )
582      )
583      precincts = load_shapefile(precincts_shp_file, g_temp_gdb)
584      #
585      # Load cross-reference table from dbf file.
586      #
587      xref_dbf_file = str(
588          Path(
589              g_proj_dir,
590              "Precincts",
591              "Precinct_Cross_Reference.dbf"
592          )
593      )
594      xref = load_table(xref_dbf_file, g_temp_gdb)
595      #
596      # Create a table to hold exploded cross-references.
597      #
598      xref_explode = create_table(g_temp_gdb, "xref_explode")
599      fields_to_add = [
600          ["Precinct", "TEXT", "", "255", "", ""],
601          ["VotePrec", "TEXT", "", "255", "", ""]
602      ]
603      xref_explode = add_fields(xref_explode, fields_to_add)
604      #
605      # Explode the cross-references.
606      #
607      xref_explode = explode_xref(xref, xref_explode)
608      #
609      # Join the exploded cross-references to the precincts.
610      #
611      precincts = join_by_field(
612          precincts, "Precinct", xref_explode, "Precinct",
613          ["VotePrec"])
614      #
615      # Update precincts with no voting precinct.
616      #
617      update_precincts(precincts)
618      #
619      # Dissolve the precincts to get the voting precincts.
620      #
621      voting_precincts = full_path(g_out_gdb, "voting_precincts")
622      voting_precincts = dissolve(
623          precincts, voting_precincts, "VotePrec")
624      print(arcpy.management.GetCount(voting_precincts),
625            "voting precincts created.")
626      print(voting_precincts)
627  
628  
629  if __name__ == '__main__':
630      #
631      # Define locations of geodatabases.
632      #
633      my_proj_dir = r"C:\ArcGIS_local_projects\SantaCruzPrecincts"
634      my_in_gdb = "SantaCruzPrecincts.gdb"
635      #
636      # Intermediate results are written to the temporary geo-database.
637      # To speed processing and avoid writing intermediate results to
638      # disk this is set to "memory".
639      # If you need to keep these results, change this to the full path of
640      # a file geodatabase as follows:
641      # my_temp_gdb = full_path(my_proj_dir, "SantaCruzPrecincts.gdb")
642      #
643      my_temp_gdb = "memory"
644      my_out_gdb = "SantaCruzPrecincts.gdb"
645      setup_env(my_proj_dir, my_in_gdb, my_temp_gdb, my_out_gdb)
646      #
647      # Run the main process.
648      #
649      run_tools()
650  
651  # ----------------------------------------------------------------------
652  # Sample Output
653  # ----------------------------------------------------------------------
654  # "C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe" C:\ArcGIS_local_projects\SantaCruzPrecincts\santa_cruz_precincts.py
655  # ...project directory: C:\ArcGIS_local_projects\SantaCruzPrecincts
656  # ...input geo-database: C:\ArcGIS_local_projects\SantaCruzPrecincts\SantaCruzPrecincts.gdb
657  # ...temporary geo-database: memory
658  # ...output geo_database: C:\ArcGIS_local_projects\SantaCruzPrecincts\SantaCruzPrecincts.gdb
659  # ...load_shapefile, shape_file C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precincts.shp
660  # ...load_shapefile, out_gdb: memory
661  # C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precincts.shp Successfully converted:  memory\Precincts
662  # Start Time: Wednesday, May 15, 2024 4:15:29 PM
663  # C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precincts.shp Successfully converted:  memory\Precincts
664  # Succeeded at Wednesday, May 15, 2024 4:15:31 PM (Elapsed Time: 1.22 seconds)
665  # ...load_shapefile, 720 precincts loaded from shapefile.
666  # ...load_shapefile, ret_val: memory\Precincts
667  # ...load_table, dbf_file C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precinct_Cross_Reference.dbf
668  # ...load_table, out_gdb: memory
669  # Converted C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precinct_Cross_Reference.dbf to memory\Precinct_Cross_Reference successfully.
670  # Start Time: Wednesday, May 15, 2024 4:15:31 PM
671  # Converted C:\ArcGIS_local_projects\SantaCruzPrecincts\Precincts\Precinct_Cross_Reference.dbf to memory\Precinct_Cross_Reference successfully.
672  # Succeeded at Wednesday, May 15, 2024 4:15:31 PM (Elapsed Time: 0.62 seconds)
673  # ...load_table, 138 cross_reference records loaded from dbf file.
674  # ...load_table, ret_val: memory\Precinct_Cross_Reference
675  # ...create_table, out_path memory
676  # ...create_table, out_name: xref_explode
677  # Start Time: Wednesday, May 15, 2024 4:15:31 PM
678  # Succeeded at Wednesday, May 15, 2024 4:15:31 PM (Elapsed Time: 0.01 seconds)
679  # ...create_table, ret_val: memory\xref_explode
680  # ...add_fields, in_table memory\xref_explode
681  # ...add_fields, field_description: [['Precinct', 'TEXT', '', '255', '', ''], ['VotePrec', 'TEXT', '', '255', '', '']]
682  # Start Time: Wednesday, May 15, 2024 4:15:31 PM
683  # Adding Precinct to xref_explode...
684  # Adding VotePrec to xref_explode...
685  # Succeeded at Wednesday, May 15, 2024 4:15:31 PM (Elapsed Time: 0.01 seconds)
686  # ...add_fields, ret_val: memory\xref_explode
687  # ...explode_xref, xref_table memory\Precinct_Cross_Reference
688  # ...explode_xref, xref_explode_table: memory\xref_explode
689  # ...explode_xref, ret_val: memory\xref_explode
690  # ...explode_xref, 561 regular precincts have a voting precinct assigned.
691  # ...join_field, in_data memory\Precincts
692  # ...join_field, in_field: Precinct
693  # ...join_field, join_table: memory\xref_explode
694  # ...join_field, fields: ['VotePrec']
695  # ...join_field, index_join_fields: NEW_INDEXES
696  # Start Time: Wednesday, May 15, 2024 4:15:31 PM
697  # Succeeded at Wednesday, May 15, 2024 4:15:32 PM (Elapsed Time: 0.10 seconds)
698  # ...join_field, 720 records after join.
699  # ...join_field, ret_val: memory\Precincts
700  # ...update_precincts, precincts_fc memory\Precincts
701  # ...update_precincts, 159 rows updated.
702  # ...update_precincts, ret_val: memory\Precincts
703  # ...update_precincts, 720 regular precincts with a voting precinct assigned after update.
704  # ...dissolve, in_features memory\Precincts
705  # ...dissolve, out_feature_class: C:\ArcGIS_local_projects\SantaCruzPrecincts\SantaCruzPrecincts.gdb\voting_precincts
706  # ...dissolve, dissolve_field: VotePrec
707  # Start Time: Wednesday, May 15, 2024 4:15:32 PM
708  # Sorting Attributes...
709  # Dissolving...
710  # Succeeded at Wednesday, May 15, 2024 4:15:33 PM (Elapsed Time: 0.91 seconds)
711  # ...dissolve, 297 records after dissolve.
712  # ...dissolve, ret_val: C:\ArcGIS_local_projects\SantaCruzPrecincts\SantaCruzPrecincts.gdb\voting_precincts
713  # 297 voting precincts created.
714  # C:\ArcGIS_local_projects\SantaCruzPrecincts\SantaCruzPrecincts.gdb\voting_precincts
715  #
716  # Process finished with exit code 0
717  # ----------------------------------------------------------------------
718