-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathImpactAnalysis.py
857 lines (710 loc) · 39.5 KB
/
ImpactAnalysis.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
"""
-------------------------------------------------------------------------------
| Copyright 2016 Esri
|
| Licensed under the Apache License, Version 2.0 (the "License");
| you may not use this file except in compliance with the License.
| You may obtain a copy of the License at
|
| http://www.apache.org/licenses/LICENSE-2.0
|
| Unless required by applicable law or agreed to in writing, software
| distributed under the License is distributed on an "AS IS" BASIS,
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
| See the License for the specific language governing permissions and
| limitations under the License.
------------------------------------------------------------------------------
"""
import arcpy
import sys
import os
# Anticipated inputs ...
# 1. Analysis Type: Choose from: Basic Proximity, Feature Comparison, Distance
# 2. Analysis Layer: Feature class to be analyzed
# 3. Output fields: from the selected input Analysis layer, which fields should be included in the output result
# 4. Related Table (optional): If the Analysis Layer has related tables,
# optionally choose a table to report information from
# 5. Related Field (optional): If a Related Table is selected, one field from the table to report values from
# -. Group like records: merge records in the results so that only
# unique combinations of attributes appear in the output
# 6. Area of Interest: Project location. Feature Class. Point, line or polygon.
# 7. Buffer shape (optional): pre-analyzed buffer for project area.
# 8. Reporting Units: based on the analysis layer shape type
# 9. Output table: the GDB path and name of the final results table
# Interim results will be written to the Scratch workspace
args = sys.argv
analysis_type = args[1]
input_analysis_layer = args[2]
output_fields = args[3]
related_table = args[4]
related_field = args[5]
# group_output_records = args[4]
group_output_records = True # This should just always happen for the released script
input_aoi = args[6]
input_buffer_layer = args[7]
reporting_units = args[8]
output_table = args[9]
# Termporary feature classes created during script execution
interim_output_aoi = "interim_result_aoi"
interim_output_buffer = "interim_result_buffer"
interim_output_merged = "interim_result"
interim_aoi_lines = "interim_aoi_lines"
interim_analysis_key = "interim_analysis_layer"
interim_related_result = "interim_result_related"
interim_output_intersect = "interim_output_intersect"
# arcpy.env.overwriteOutput = True # should be set by the user in Geoprocessing Options
output_workspace = arcpy.env.scratchWorkspace
arcpy.env.workspace = output_workspace
arcpy.AddMessage("Analysis Type: {}".format(analysis_type))
arcpy.AddMessage("Input analysis layer: {}".format(input_analysis_layer))
arcpy.AddMessage("Output fields: {}".format(output_fields))
arcpy.AddMessage("Related table: {}".format(related_table))
arcpy.AddMessage("Related field: {}".format(related_field))
arcpy.AddMessage("Group like records: {}".format(group_output_records))
arcpy.AddMessage("Input AOI: {}".format(input_aoi))
arcpy.AddMessage("Input buffer: {}".format(input_buffer_layer))
arcpy.AddMessage("Reporting units: {}".format(reporting_units))
arcpy.AddMessage("Output table: {}".format(output_table))
arcpy.AddMessage("- - - - - - - - - - - - - - - - - - - ")
# Get units of measure squared away
area_units = reporting_units
if reporting_units in ["Meters", "Kilometers"]: # then we need a different unit for area calculations
area_units = "SQUAREKILOMETERS"
elif reporting_units in ["Feet", "Miles"]:
area_units = "ACRES"
# Related Table Variables
foreign_key = ""
primary_key = ""
aoi_key_values = ""
buffer_key_values = ""
origin_OID_field = ""
related_table_full_path = ""
# --------------------------------------
# Function to validate script inputs
def validate_inputs():
reason = ""
if analysis_type in ["Feature Comparison", "Basic Proximity", "Distance"]:
result = True
else:
result = False
reason = 'Analysis type must be one of: "Feature Comparison", "Basic Proximity", "Distance"'
if arcpy.Exists(input_analysis_layer):
fields = arcpy.ListFields(input_analysis_layer)
field_names = []
for field in fields:
field_names.append(field.name.upper())
check_fields = output_fields.split(';')
for item in check_fields:
if item.upper() in field_names:
continue
else:
result = False
reason += "\n No field {} found in {}".format(item, input_analysis_layer)
else:
result = False
reason += '\n {} does not exist'.format(input_analysis_layer)
if not arcpy.Exists(input_aoi):
result = False
reason += '\n {} does not exist'.format(input_aoi)
if input_buffer_layer != "#":
if not arcpy.Exists(input_buffer_layer):
result = False
reason += '\n {} does not exist'.format(input_buffer_layer)
return [result, reason]
# --------------------------------------
# Get related table information
def check_related_records(input_fc, rel_table):
try:
global foreign_key
global primary_key
global origin_OID_field
global related_table_full_path
arcpy.AddMessage("Collecting related table information")
fc_desc = arcpy.Describe(input_fc)
for j,rel in enumerate(fc_desc.relationshipClassNames):
rel_desc = arcpy.Describe(os.path.dirname(fc_desc.catalogPath) + "\\" + rel)
if rel_desc.isAttachmentRelationship:
continue
destination = rel_desc.destinationClassNames
if rel_table in destination:
for key_name, key_role, unk in rel_desc.originClassKeys:
# print "Name: {} Role: {}".format(key_name, key_role)
if key_role == "OriginForeign":
foreign_key = key_name
elif key_role == "OriginPrimary":
primary_key = key_name
# get the full path for the related table
dir_name = os.path.dirname(fc_desc.catalogPath)
dir_desc = arcpy.Describe(dir_name)
if hasattr(dir_desc, "datasetType") and dir_desc.datasetType == "FeatureDataset":
dir_name = os.path.dirname(dir_name)
related_table_full_path = dir_name + "\\" + related_table
# check if the primary key is the OID, if it is, we need to kick it back
if fc_desc.hasOID:
origin_OID_field = fc_desc.OIDFieldName
if fc_desc.OIDFieldName.lower() == primary_key.lower():
# need to skip this
arcpy.AddWarning("Check Related Records: The primary key for the selected related table and "
"field is the ObjectID. ObjectID field types are not supported as primary keys "
"for {} Analysis. Related records will not be analyzed".format(analysis_type))
foreign_key = ""
primary_key = ""
return ""
# check if the primary key is a GlobalID, if it is, we need to copy the dataset preserving the global id value
primary_key_field = arcpy.ListFields(input_fc, primary_key)[0]
if primary_key_field.type == "GlobalID":
field_mapping = arcpy.FiledMappings()
for field in arcpy.ListFields(input_fc):
if field.type != "OID" and field.type != "GlobalID":
fm = arcpy.FieldMap()
fm.addInputField(input_fc, field.name)
field_mapping.addFieldMap(fm)
# create a mapping to map the GlobalID into
fm = arcpy.FieldMap()
fm.addInputField(input_fc, primary_key_field.name)
fm.mergeRule = 'First'
f_name = fm.outputField
f_name.name = "ANALYSISKEY"
f_name.aliasName = "Relationship Key Field"
f_name.type = "Guid"
fm.outputField = f_name
field_mapping.addFieldMap(fm)
arcpy.CopyFeatures_management(input_fc, interim_analysis_key)
# Update the primary key field to the copy --
primary_key = "ANALYSISKEY"
return output_workspace + "\\" + interim_analysis_key
else:
return input_fc
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Check Related Records: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Get key values for related records - currently not required
def get_key_values(selected_layer, analysis_layer, layer_type):
try:
global aoi_key_values
global buffer_key_values
arcpy.AddMessage("Collecting related record key values ({}).".format(layer_type))
selected_layer = arcpy.Describe(selected_layer)
selected_ids = selected_layer.FIDset
selected_ids = selected_ids.split(';')
selected_ids = ", ".join(selected_ids)
# arcpy.AddMessage("Selected IDs: {}".format(selected_ids))
if layer_type == "AOI":
aoi_key_values = [row[0] for row in arcpy.da.SearchCursor(analysis_layer, primary_key,
"{0} IN ({1})".format(origin_OID_field,
selected_ids))]
# arcpy.AddMessage("Selected AOI primary key values: {}".format(aoi_key_values))
else:
buffer_key_values = [row[0] for row in arcpy.da.SearchCursor(analysis_layer, primary_key,
"{0} IN ({1})".format(origin_OID_field,
selected_ids))]
# arcpy.AddMessage("Selected Buffer primary key values: {}".format(buffer_key_values))
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Get Key Values: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Basic Proximity Analysis Type
def basic_proximity(analysis_layer, select_by_layer, out_layer_name, layer_type):
try:
out_layer = output_workspace + "\\" + out_layer_name
arcpy.MakeFeatureLayer_management(analysis_layer, "basic_proximity")
arcpy.SelectLayerByLocation_management("basic_proximity", 'intersect', select_by_layer)
match_count = int(arcpy.GetCount_management("basic_proximity")[0])
if match_count == 0:
arcpy.AddMessage(("No features found in {0}".format(analysis_layer)))
return "#"
else:
# Removed Get Key Values call - if primary key is persisted through the result, this shouldn't be needed
# # If a field from a related table has been identified, then get a list of key values
# if primary_key:
# get_key_values(out_layer_name, analysis_layer, layer_type)
arcpy.AddMessage(("{0} features found in {1}".format(match_count, analysis_layer)))
arcpy.CopyFeatures_management("basic_proximity", out_layer)
arcpy.AddField_management(out_layer, "ANALYSISTYPE", "TEXT", "", "", 10, "Analysis Result Type")
exp = "'{}'".format(layer_type)
arcpy.CalculateField_management(out_layer, "ANALYSISTYPE", exp, "PYTHON_9.3", None)
return out_layer
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Basic Proximity Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Feature Comparison Analysis Type
def feature_comparison(analysis_layer, clip_layer, out_layer_name, layer_type, clip_area, aoi_shape_type):
try:
xy_tolerance = ""
out_layer = output_workspace + "\\" + out_layer_name
# at pro 2.2 and possibly back to 2.1.3 if input features do not intersect the extent of the clip feature
# the clip tool will fail rather than throw the expected warning about empty output
# try/except were added to avoid the failure
# intersect does not throw the same failure so it is checked to verify that no features intersect the clip feature
# so we can avoid being concerened about why the clip failed
try:
arcpy.Clip_analysis(analysis_layer, clip_layer, out_layer, xy_tolerance)
except Exception as error:
out_intersect_layer = interim_output_intersect
r = arcpy.Intersect_analysis([analysis_layer, clip_layer], out_intersect_layer, "ONLY_FID", None, "INPUT")
warnings = r.getMessages(1)
# 000117 is the code for empty output
if warnings and '000117' in warnings:
pass
# at Pro 1.3, if nothing intersects with the clip layer, no result is generated - account for this
if not arcpy.Exists(out_layer_name):
arcpy.AddMessage(("No {0} features found in {1} (No clip result found).".format(analysis_layer, clip_layer)))
return "#"
match_count = int(arcpy.GetCount_management(out_layer_name)[0])
if match_count == 0:
arcpy.AddMessage(("No {0} features found in {1}.".format(analysis_layer, clip_layer)))
return "#"
else:
arcpy.AddMessage(("Processing {0} features in {1} within {2}.".format(match_count, analysis_layer, layer_type)))
desc = arcpy.Describe(out_layer)
analysis_shape_type = desc.shapeType
if analysis_shape_type == "Polygon":
arcpy.AddField_management(out_layer, "ANALYSISAREA", "DOUBLE", "", "", "",
"Total Area ({})".format(reporting_units))
exp = '!shape.geodesicArea@{}!'.format(reporting_units)
arcpy.CalculateField_management(out_layer, "ANALYSISAREA", exp, "PYTHON_9.3", None)
arcpy.AddField_management(out_layer, "ANALYSISPERCENT", "DOUBLE", "", "", "", "Percent of Area")
exp = '(!ANALYSISAREA! / ' + str(clip_area) + ')*100'
arcpy.CalculateField_management(out_layer, "ANALYSISPERCENT", exp, "PYTHON_9.3", None)
elif analysis_shape_type == "Polyline":
arcpy.AddField_management(out_layer, "ANALYSISLEN", "DOUBLE", "", "", "",
"Total Length ({})".format(reporting_units))
exp = '!shape.geodesicLength@{}!'.format(reporting_units)
arcpy.CalculateField_management(out_layer, "ANALYSISLEN", exp, "PYTHON_9.3", None)
elif analysis_shape_type == "Point":
arcpy.AddField_management(out_layer, "ANALYSISCOUNT", "SHORT", "", "", "", "Count of Features")
exp = '1'
arcpy.CalculateField_management(out_layer, "ANALYSISCOUNT", exp, "PYTHON_9.3", None)
else:
arcpy.AddMessage("Shape type not supported: {}".format(analysis_shape_type))
if aoi_shape_type == "Polygon":
# If the AOI is a point or line, then the only results will be with the buffer,
# don't need to differentiate between AOI or buffer in this case
arcpy.AddField_management(out_layer, "ANALYSISTYPE", "TEXT", "", "", 10, "Analysis Result Type")
exp = "'{}'".format(layer_type)
arcpy.CalculateField_management(out_layer, "ANALYSISTYPE", exp, "PYTHON_9.3", None)
return out_layer
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Feature Comparison Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Distance Analysis Type -- for the Area of Interest layer
def distance_analysis_aoi(near_layer, aoi_layer, out_layer_name):
try:
# Find everything intersecting the AOI
arcpy.MakeFeatureLayer_management(near_layer, "near_layer")
arcpy.SelectLayerByLocation_management("near_layer", 'intersect', aoi_layer)
match_count = int(arcpy.GetCount_management("near_layer")[0])
if match_count == 0:
arcpy.AddMessage(("No features found in {0}".format(aoi_layer)))
return "#"
else:
arcpy.AddMessage(("{0} features found in {1}. Calculating distances.".format(match_count, aoi_layer)))
arcpy.CopyFeatures_management("near_layer", out_layer_name)
desc = arcpy.Describe(aoi_layer)
input_shape_type = desc.shapeType
desc_near_layer = arcpy.Describe(out_layer_name).spatialReference
global reporting_units
reporting_units = desc_near_layer.linearUnitName
if input_shape_type == "Polygon":
arcpy.PolygonToLine_management(aoi_layer, interim_aoi_lines)
arcpy.Near_analysis(out_layer_name, interim_aoi_lines, None, "NO_LOCATION", "ANGLE", "GEODESIC")
else:
arcpy.Near_analysis(out_layer_name, aoi_layer, None, "NO_LOCATION", "ANGLE", "GEODESIC")
arcpy.AddField_management(out_layer_name, "ANALYSISLOC", "TEXT", "", "", 100, "Location")
exp = "getValue(!NEAR_DIST!)"
codeblock = """def getValue(dist):
dist = round(dist, 4)
if dist == 0:
return 'Intersecting with AOI boundary'
else:
return 'Within AOI'"""
arcpy.CalculateField_management(out_layer_name, "ANALYSISLOC", exp, "PYTHON_9.3", codeblock)
exp = "getValue(!NEAR_DIST!, !NEAR_ANGLE!)"
codeblock = """def getValue(dist, angle):
dist = round(dist, 4)
if dist == 0:
return 0
else:
return angle"""
arcpy.CalculateField_management(out_layer_name, "NEAR_ANGLE", exp, "PYTHON_9.3", codeblock)
return out_layer_name
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Distance Analysis (AOI) Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Function to abbreviate units of measure for readability
def abbreviate_units(units):
try:
lower_units = units.lower()
return {'acres': 'acres',
'hectares': 'hectares',
'squaremiles': 'sq mi',
'squarekilometers': 'sq km',
'squaremeters': 'sq m',
'squarefeet': 'sq ft',
'miles': 'mi',
'kilometers': 'km',
'meters': 'm',
'meter': 'm',
'us-foot': 'ft',
'foot_us': 'ft',
'feet': 'ft'
}.get(lower_units, units)
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Abbreviate Units Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Distance Analysis Type -- for the Buffer layer
def distance_analysis_buffer(near_layer, aoi_layer, buffer_layer, out_layer_name):
try:
# Find everything intersecting the buffer
arcpy.MakeFeatureLayer_management(near_layer, "near_layer")
arcpy.SelectLayerByLocation_management("near_layer", 'intersect', buffer_layer)
# Remove from the selection everything intersecting the AOI (these have already been accounted for)
arcpy.SelectLayerByLocation_management("near_layer", 'intersect', aoi_layer, None, "REMOVE_FROM_SELECTION")
match_count = int(arcpy.GetCount_management("near_layer")[0])
if match_count == 0:
arcpy.AddMessage(("No features found in {0}".format(buffer_layer)))
return "#"
else:
arcpy.AddMessage(("{0} features found in {1}. Calculating distances.".format(match_count, buffer_layer)))
arcpy.CopyFeatures_management("near_layer", out_layer_name)
desc = arcpy.Describe(aoi_layer)
input_shape_type = desc.shapeType
desc_near_layer = arcpy.Describe(out_layer_name).spatialReference
global reporting_units
reporting_units = desc_near_layer.linearUnitName
if input_shape_type == "Polygon":
arcpy.PolygonToLine_management(aoi_layer, interim_aoi_lines)
arcpy.Near_analysis(out_layer_name, interim_aoi_lines, None, "NO_LOCATION", "ANGLE", "GEODESIC")
else:
arcpy.Near_analysis(out_layer_name, aoi_layer, None, "NO_LOCATION", "ANGLE", "GEODESIC")
arcpy.AddField_management(out_layer_name, "ANALYSISLOC", "TEXT", "", "", 100, "Location")
exp = "getValue(!NEAR_DIST!)"
codeblock = """def getValue(dist):
if dist > 0:
return 'Outside AOI, within buffer'
else:
return 'Unexpected result'"""
arcpy.CalculateField_management(out_layer_name, "ANALYSISLOC", exp, "PYTHON_9.3", codeblock)
return out_layer_name
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Distance Analysis (Buffer) Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Function to calculate areas
def get_area(input_fc, units):
try:
geometries = arcpy.CopyFeatures_management(input_fc, arcpy.Geometry())
area = 0
for geometry in geometries:
area += geometry.getArea('GEODESIC', units)
return area
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Get Area Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Function to format outputs
def format_outputs(output_layer, out_fields):
try:
field_mapper = arcpy.FieldMappings()
fields = arcpy.ListFields(output_layer)
compare_fields = []
stat_fields = ""
out_fields_lower = out_fields.lower()
for field in fields:
field_name_lower = field.name
field_name_lower = field_name_lower.lower()
if field_name_lower in out_fields_lower and field.type != "Geometry":
# Keep Me
fm = arcpy.FieldMap()
fm.addInputField(output_layer, field.name)
field_mapper.addFieldMap(fm)
compare_fields.append(field.name)
elif field.name in ['ANALYSISPERCENT', 'ANALYSISAREA', 'ANALYSISLEN', 'ANALYSISCOUNT']:
# Keep Me -- these are statistics fields
fm = arcpy.FieldMap()
fm.addInputField(output_layer, field.name)
field_mapper.addFieldMap(fm)
if stat_fields == "":
stat_fields = field.name + " SUM"
else:
stat_fields = stat_fields + ";" + field.name + " SUM"
elif field.name in ['ANALYSISTYPE', 'ANALYSISLOC', 'NEAR_DIST', 'NEAR_ANGLE']:
# Keep Me -- these are informational/descriptive fields
fm = arcpy.FieldMap()
fm.addInputField(output_layer, field.name)
field_mapper.addFieldMap(fm)
compare_fields.append(field.name)
elif field.name in primary_key:
# Keep Me -- need this for related records
fm = arcpy.FieldMap()
fm.addInputField(output_layer, field.name)
field_mapper.addFieldMap(fm)
compare_fields.append(field.name)
elif field.type.upper() in ["OID"]:
# Keep Me
fm = arcpy.FieldMap()
fm.addInputField(output_layer, field.name)
field_mapper.addFieldMap(fm)
else:
# Delete Me
# arcpy.AddMessage("{}: Delete Me".format(field.name))
continue
# if related records need to be included -- break here and create another interim result before the final
if primary_key:
out_path = output_workspace
out_name = interim_related_result
else:
out_path = os.path.dirname(os.path.abspath(output_table))
out_name = output_table.split("\\")[-1]
out_table = out_path + "\\" + out_name
abbreviated_units = abbreviate_units(reporting_units)
# Clean up the results by deleting or merging identical records
if analysis_type == "Basic Proximity":
# Nothing special, just remove duplicates
if group_output_records:
# Using the summary statistics tool because Remove Duplicates requires an Advanced License
arcpy.Statistics_analysis(output_layer, out_table, "ANALYSISTYPE COUNT", compare_fields)
arcpy.DeleteField_management(out_table, ["FREQUENCY"])
arcpy.DeleteField_management(out_table, ["COUNT_ANALYSISTYPE"])
else:
arcpy.TableToTable_conversion(output_layer, out_path, out_name, field_mapping=field_mapper)
elif analysis_type == "Distance":
# Don't remove duplicates, if there are 5 eagle nests, i want to know there
# are 5 and their unique distances from the project
arcpy.TableToTable_conversion(output_layer, out_path, out_name, field_mapping=field_mapper)
# Update field aliases to be readable and have distance units embedded
arcpy.AlterField_management(out_table, "NEAR_DIST", None,
"Distance ({})".format(abbreviated_units))
arcpy.AlterField_management(out_table, "NEAR_ANGLE", None, "Direction")
else:
# Feature Comparison -- summarize the output layer based on the 'keep fields'
# Possible Stat Fields .. 'ANALYSISPERCENT', 'ANALYSISAREA', 'ANALYSISLEN', 'ANALYSISCOUNT'
if group_output_records:
arcpy.Statistics_analysis(output_layer, out_table, stat_fields, compare_fields)
arcpy.DeleteField_management(out_table, ["FREQUENCY"])
if "ANALYSISPERCENT" in stat_fields:
arcpy.AlterField_management(out_table, "SUM_ANALYSISPERCENT", "ANALYSISPERCENT", "Percent of Area")
arcpy.AlterField_management(out_table, "SUM_ANALYSISAREA", "ANALYSISAREA",
"Total Area ({})".format(abbreviated_units))
elif "ANALYSISLEN" in stat_fields:
arcpy.AlterField_management(out_table, "SUM_ANALYSISLEN", "ANALYSISLEN",
"Total Length ({})".format(abbreviated_units))
else:
arcpy.AddField_management(out_table, "ANALYSISCOUNT", "SHORT", "", "", "", "Count of Features")
exp = '!SUM_ANALYSISCOUNT!'
arcpy.CalculateField_management(out_table, "ANALYSISCOUNT", exp, "PYTHON_9.3", None)
arcpy.DeleteField_management(out_table, "SUM_ANALYSISCOUNT")
else:
arcpy.TableToTable_conversion(output_layer, out_path, out_name, field_mapping=field_mapper)
# if related records need to be included -- add related values to result table
if primary_key:
arcpy.AddMessage("Adding related table values to result.")
query_related_fields = []
# create a new table with same fields as the interim result
out_path = os.path.dirname(os.path.abspath(output_table))
out_name = output_table.split("\\")[-1]
# arcpy.AddMessage("New output table: {}\\{}".format(out_path, out_name))
# arcpy.AddMessage("Template table: {}".format(interim_related_result))
arcpy.CreateTable_management(out_path, out_name, interim_related_result, None)
# Get the field properties from the related table, must get them all
field_props = arcpy.ListFields(related_table_full_path)
# Add a field to the result table to contain the selected related table field values
for field in field_props:
# arcpy.AddMessage("CURRENT field: {}".format(field.name))
if field.name in related_field:
# arcpy.AddMessage("found a related field -- adding it to output")
query_related_fields.append(field.name)
arcpy.AddField_management(output_table, field.name, field.type,
field.precision, field.scale,
field.length, field.aliasName,
"NULLABLE", "NON_REQUIRED", None)
# prep an insert cursor for the final results table
result_fields = arcpy.ListFields(output_table)
result_field_names = []
empty_result_value = []
for field in result_fields:
# if the field is the OID skip it
if field.type != "OID":
result_field_names.append(field.name)
# if the current field is the analysis type field or related table field -
# then do not add an empty value for it
if field.name != "ANALYSISTYPE" and field.name not in related_field:
empty_result_value.append(None)
# arcpy.AddMessage("Result fields: {}".format(result_field_names))
# Prep parameters to be used during the process
foreign_key_formatted = arcpy.AddFieldDelimiters(arcpy.Describe(related_table_full_path).path, foreign_key)
foreign_key_type = arcpy.ListFields(related_table_full_path, foreign_key)[0].type
# cycle through the interim result table
with arcpy.da.SearchCursor(interim_related_result, "*") as records:
fields = records.fields
key_field_index = fields.index(primary_key)
if "ANALYSISTYPE" in result_field_names:
analysis_type_index = fields.index("ANALYSISTYPE")
for record in records:
# query the related table to get the list of related values
current_key = record[key_field_index]
if "ANALYSISTYPE" in result_field_names:
current_analysis_type = record[analysis_type_index]
if foreign_key_type == "String":
current_key = "'{}'".format(current_key)
whereclause = "{} in ({})".format(foreign_key_formatted, current_key)
# strip OID
origin_values = list(record)
origin_values.pop(0)
# arcpy.AddMessage("Current record: {}".format(origin_values))
# arcpy.AddMessage("Related fields for query: {}".format(query_related_fields))
with arcpy.da.SearchCursor(related_table_full_path, query_related_fields, whereclause) as related_values:
# arcpy.AddMessage("result values: ".format(related_values))
with arcpy.da.InsertCursor(output_table, result_field_names) as result_cursor:
# Loop through the related values list and insert them into the final results table
for i, value in enumerate(related_values):
related_value_only = list(empty_result_value)
# arcpy.AddMessage(related_value_only)
if i == 0:
# add a record into the result table for the 'parent' record + one or many related values
origin_values.extend(value)
# arcpy.AddMessage("Values list: {}".format(origin_values))
# arcpy.AddMessage("Origin Values: {}".format(origin_values))
result_cursor.insertRow(origin_values)
else:
# add a record into the result table for each additional related value
value_list = list(value)
if "ANALYSISTYPE" in result_field_names:
# the index for the analysis type field will be off by 1 because it's based
# on the record that includes an OID field
related_value_only.insert((analysis_type_index - 1), current_analysis_type)
related_value_only.extend(value_list)
# arcpy.AddMessage(related_value_only)
result_cursor.insertRow(related_value_only)
return True
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Format Outputs Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Create the output table with information about there being no results
def create_empty_output(out_table, message_overwrite):
try:
out_path = os.path.dirname(os.path.abspath(out_table))
out_name = out_table.split("\\")[-1]
arcpy.CreateTable_management(out_path, out_name, None, None)
arcpy.AddField_management(out_table, "ANALYSISNONE", "TEXT", "", "", 150, 'Analysis Result')
rows = arcpy.InsertCursor(out_table)
row = rows.newRow()
if message_overwrite != "":
row.setValue("ANALYSISNONE", message_overwrite)
elif input_buffer_layer != "#":
row.setValue("ANALYSISNONE", "No features intersect the area of interest or buffer.")
else:
row.setValue("ANALYSISNONE", "No features intersect the area of interest.")
rows.insertRow(row)
del row
del rows
except Exception as error:
exc_type, exc_obj, exc_tb = sys.exc_info()
arcpy.AddError("Create Empty Output Error: Line {}: {}".format(exc_tb.tb_lineno, error))
# --------------------------------------
# Main
try:
arcpy.AddMessage("Begin Analysis: {}".format(analysis_type))
valid_inputs = validate_inputs()
output_message = ""
if not valid_inputs[0]:
arcpy.AddError("Invalid inputs: {}".format(valid_inputs[1]))
exit()
# if related fields are chosen, then we need to ensure the primary key is preserved
if related_field != "#" and related_field != "" and related_field != None:
input_analysis_layer = check_related_records(input_analysis_layer, related_table)
if analysis_type == "Feature Comparison":
aoi_layer_properties = arcpy.Describe(input_aoi)
aoi_shape = aoi_layer_properties.shapeType
analysis_layer_properties = arcpy.Describe(input_analysis_layer)
analysis_shape = analysis_layer_properties.shapeType
if aoi_shape in ["Point", "Polyline"]:
# check if a buffer shape was provided
if input_buffer_layer == "#":
arcpy.AddWarning("For point and polyline areas of interest, a buffer layer is required for "
"Feature Comparison analyses.")
aoi_out = "#"
buffer_out = "#"
output_message = "For point and polyline areas of interest, a buffer layer is required for " \
"Feature Comparison analyses."
else:
aoi_out = "#"
if analysis_shape == "Polygon":
buffer_area = get_area(input_buffer_layer, area_units)
else:
buffer_area = 0 # for point and polyline anlaysis layers, the buffer_area is not used
buffer_out = feature_comparison(input_analysis_layer, input_buffer_layer, interim_output_buffer,
'Buffer', buffer_area, aoi_shape)
else: # polygon area of interest layer layer
if analysis_shape == "Polygon":
aoi_area = get_area(input_aoi, area_units)
else:
aoi_area = 0
aoi_out = feature_comparison(input_analysis_layer, input_aoi, interim_output_aoi, 'AOI', aoi_area, aoi_shape)
if input_buffer_layer != "#":
if analysis_shape == "Polygon":
buffer_area = get_area(input_buffer_layer, area_units)
else:
buffer_area = 0
buffer_out = feature_comparison(input_analysis_layer, input_buffer_layer, interim_output_buffer,
'Buffer', buffer_area, aoi_shape)
else:
arcpy.AddMessage("No buffer layer provided.")
buffer_out = "#"
elif analysis_type == "Basic Proximity":
aoi_out = basic_proximity(input_analysis_layer, input_aoi, interim_output_aoi, 'AOI')
if input_buffer_layer != "#":
buffer_out = basic_proximity(input_analysis_layer, input_buffer_layer, interim_output_buffer, 'Buffer')
else:
arcpy.AddMessage("No buffer layer provided.")
buffer_out = "#"
else:
aoi_out = distance_analysis_aoi(input_analysis_layer, input_aoi, interim_output_aoi)
if input_buffer_layer != "#":
buffer_out = distance_analysis_buffer(input_analysis_layer, input_aoi,
input_buffer_layer, interim_output_buffer)
else:
arcpy.AddMessage("No buffer layer provided.")
buffer_out = "#"
arcpy.AddMessage("Creating output table.")
if (aoi_out == "#") & (buffer_out == "#"):
arcpy.AddMessage("No results found in the Area of Interest or Buffer (if provided) locations.")
create_empty_output(output_table, output_message)
elif aoi_out == "#":
arcpy.AddMessage("No results found in the Area of Interest locations.")
format_outputs(buffer_out, output_fields)
elif buffer_out == "#":
arcpy.AddMessage("No results found in the Buffer location or no buffer provided.")
format_outputs(aoi_out, output_fields)
else:
# got results from both AOI and Buffer results, merge them together before formatting...
merged_output = output_workspace + "\\" + interim_output_merged
arcpy.Merge_management([aoi_out, buffer_out], merged_output)
arcpy.AddMessage("Results found for both the Area of Interest and Buffer locations.")
format_outputs(merged_output, output_fields)
except Exception as err:
exc_m_type, exc_m_obj, exc_m_tb = sys.exc_info()
arcpy.AddError("Impact Analysis Error: Line {}: {}".format(exc_m_tb.tb_lineno, err))
finally:
# Clean up temporary files
arcpy.AddMessage("Cleaning up interim results.")
if arcpy.Exists(interim_output_aoi):
arcpy.Delete_management(interim_output_aoi)
if arcpy.Exists(interim_output_buffer):
arcpy.Delete_management(interim_output_buffer)
if arcpy.Exists(interim_output_merged):
arcpy.Delete_management(interim_output_merged)
if arcpy.Exists(interim_aoi_lines):
arcpy.Delete_management(interim_aoi_lines)
if arcpy.Exists(interim_analysis_key):
arcpy.Delete_management(interim_analysis_key)
if arcpy.Exists(interim_related_result):
arcpy.Delete_management(interim_related_result)
if arcpy.Exists(interim_output_intersect):
arcpy.Delete_management(interim_output_intersect)