From 1bede7737650ca50be0e374fd23b2c7c38e83df4 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 15 Jun 2026 13:42:40 -0400 Subject: [PATCH 01/42] Driveby: rename --- scripts/Utilities/GeocodeAddress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Utilities/GeocodeAddress.py b/scripts/Utilities/GeocodeAddress.py index 13d5fc9..1b5d9a2 100644 --- a/scripts/Utilities/GeocodeAddress.py +++ b/scripts/Utilities/GeocodeAddress.py @@ -15,7 +15,7 @@ class GeocodeAddress(object): def __init__(self): """Define the tool (tool name is the name of the class).""" - self.label = "NY Geocode Address" + self.label = "Geocode Address (NY)" self.description = "Geocode NY address to point" self.category = "Utilities" self.canRunInBackground = False From d4b0f98e5e030ce77bc1b7c2592de2fa634c40c0 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 15 Jun 2026 13:54:24 -0400 Subject: [PATCH 02/42] polygon centerline boilerplate --- SWCD Tools.pyt | 1 + .../FluvialGeomorphology/PolygonCenterline.py | 123 ++++++++++++++++++ scripts/FluvialGeomorphology/__init__.py | 1 + 3 files changed, 125 insertions(+) create mode 100644 scripts/FluvialGeomorphology/PolygonCenterline.py diff --git a/SWCD Tools.pyt b/SWCD Tools.pyt index 7a8ddad..60bad9a 100644 --- a/SWCD Tools.pyt +++ b/SWCD Tools.pyt @@ -18,6 +18,7 @@ class Toolbox(object): StreamNetwork, StreamElevation, GenerateCrossSections, + PolygonCenterline, LocalMinimums, RelativeElevationModel, TopographicPositionIndex, diff --git a/scripts/FluvialGeomorphology/PolygonCenterline.py b/scripts/FluvialGeomorphology/PolygonCenterline.py new file mode 100644 index 0000000..f0b3643 --- /dev/null +++ b/scripts/FluvialGeomorphology/PolygonCenterline.py @@ -0,0 +1,123 @@ +# -------------------------------------------------------------------------------- +# Name: Polygon Centerline +# Purpose: This tool creates a polygon centerline optionally intersected to +# edge points. This is a non-restrictively licensed alternative to +# the Topographic Production Tools license "Polygon to Centerline" tool. +# +# License: Contextual Copyleft AI (CCAI) License v1.0. +# Full license in LICENSE file. +# -------------------------------------------------------------------------------- + +import os +import math +import arcpy + +from ..helpers import license, reload_module, log, empty_workspace +from ..helpers import setup_environment as setup +from ..helpers import validate_spatial_reference as validate + +def polygon_centerline(polygon, edge_points): + """ TODO + line - arcpy.PolyLine() object + interval - + """ + interval, interval_unit = interval.split(" ") + transects = [] + + # positionAlongLine fails to get start point dist=0 if using geodesic=True + # so we use geodesic=False which uses Meters as linear unit instead of line unit + interval = int(float(interval) * arcpy.LinearUnitConversionFactor(interval_unit, "Meters")) + length = line.getLength("GEODESIC", units="Meters") + for dist in range(0, int(length)+interval, interval): + # get point at distance + point = line.positionAlongLine(dist, geodesic=False)[0] + + #create transect at point + transect = transect_line(line, point, width) + transects.append(transect) + + return transects + +class PolygonCenterline(object): + def __init__(self): + """Define the tool (tool name is the name of the class).""" + self.label = "Polygon Centerline" + self.description = "Generate polygon centerline" + self.category = "Fluvial Geomorphology" + self.canRunInBackground = False + + def getParameterInfo(self): + """Define parameter definitions""" + param0 = arcpy.Parameter( + displayName="Polygon", + name="polygon", + datatype="GPFeatureLayer", + parameterType="Required", + direction="Input") + param0.filter.list = ["Polygon"] + + param1 = arcpy.Parameter( + displayName="Connecting Edge Points", + name="points", + datatype="GPFeatureLayer", + parameterType="Optional", + direction="Input") + param1.filter.list = ["Point"] + + # TODO: fill holes option + + param2 = arcpy.Parameter( + displayName="Output Features", + name="out_features", + datatype="DEFeatureClass", + parameterType="Required", + direction="Output") + param2.parameterDependencies = [param0.name] + param2.schema.clone = True + + params = [param0, param1, param2] + return params + + def updateParameters(self, parameters): + return + + def isLicensed(self): + """Set whether the tool is licensed to execute.""" + return license() + + def updateMessages(self, parameters): + """Modify the messages created by internal validation for each tool parameter.""" + validate(parameters) + return + + @reload_module(__name__) + def execute(self, parameters, messages): + """The source code of the tool.""" + # Setup + log("setting up project") + project, active_map = setup() + + # read in parameters + polygon = parameters[0].value + edge_points = parameters[1].value + output_file = parameters[2].valueAsText + + # TODO: handle multiple polygons / points + + # find centerline + centerline = polygon_centerline(polygon, edge_points) + arcpy.conversion.ExportFeatures(centerline, output_file) + + # add data + log("adding data") + active_map.addDataFromPath(output_file) + + # cleanup + log("deleting unneeded data") + empty_workspace(arcpy.env.scratchGDB) + + # save project + log("saving project") + project.save() + + return diff --git a/scripts/FluvialGeomorphology/__init__.py b/scripts/FluvialGeomorphology/__init__.py index 082a16a..1403e98 100644 --- a/scripts/FluvialGeomorphology/__init__.py +++ b/scripts/FluvialGeomorphology/__init__.py @@ -11,3 +11,4 @@ from .StreamNetwork import StreamNetwork from .StreambankDetection import StreambankDetection from .GenerateCrossSections import GenerateCrossSections, generate_transects, transect_line +from .PolygonCenterline import PolygonCenterline, polygon_centerline From 5fe2ac265a8b75fd167bfbbbf74a00332a53f22f Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 15 Jun 2026 16:11:06 -0400 Subject: [PATCH 03/42] boilerplate --- SWCD Tools.pyt.xml | 2 +- .../FluvialGeomorphology/PolygonCenterline.py | 59 ++++++++++++------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/SWCD Tools.pyt.xml b/SWCD Tools.pyt.xml index 4e1043b..dc10677 100644 --- a/SWCD Tools.pyt.xml +++ b/SWCD Tools.pyt.xml @@ -1,2 +1,2 @@ -20251119093401001.0TRUE202604071830011500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 +20251119093401001.0TRUE202606151510141500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 diff --git a/scripts/FluvialGeomorphology/PolygonCenterline.py b/scripts/FluvialGeomorphology/PolygonCenterline.py index f0b3643..bfe4ebd 100644 --- a/scripts/FluvialGeomorphology/PolygonCenterline.py +++ b/scripts/FluvialGeomorphology/PolygonCenterline.py @@ -17,26 +17,45 @@ from ..helpers import validate_spatial_reference as validate def polygon_centerline(polygon, edge_points): - """ TODO - line - arcpy.PolyLine() object - interval - - """ - interval, interval_unit = interval.split(" ") - transects = [] - - # positionAlongLine fails to get start point dist=0 if using geodesic=True - # so we use geodesic=False which uses Meters as linear unit instead of line unit - interval = int(float(interval) * arcpy.LinearUnitConversionFactor(interval_unit, "Meters")) - length = line.getLength("GEODESIC", units="Meters") - for dist in range(0, int(length)+interval, interval): - # get point at distance - point = line.positionAlongLine(dist, geodesic=False)[0] - - #create transect at point - transect = transect_line(line, point, width) - transects.append(transect) - - return transects + """Find centerline of polygon.""" + + # TODO: densify + arcpy.edit.Densify( + in_features="vbet_SimplifyPolygon1", + densification_method="DISTANCE", + distance="50 Meters", + max_deviation="0.1 Meters", + max_angle=10, + max_vertex_per_segment=None + ) + + # TODO: thiessen + + # TODO: polygon to line + + # TODO: select by location: completely within + + # TODO: Export to new fc + + # TODO: Dissolve (single, unsplit) + + # TODO: feat vert to pts (dangling) + + # TODO: Selct by location (itnersect pt and line) + + # TODO: Delete selected + + # TODO: Generate near table + + # TODO: XY to line from table + + # TODO: Merge line with centerline + + # TODO: Select by location (completely within VBET) + + # TODO: Delete selected + + return centerline class PolygonCenterline(object): def __init__(self): From 572f7ba49655414f2509821a7594bc6327e658b0 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 22 Jun 2026 13:59:55 -0400 Subject: [PATCH 04/42] update comment --- scripts/Utilities/RemoveUnused.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Utilities/RemoveUnused.py b/scripts/Utilities/RemoveUnused.py index 6055b7c..988b700 100644 --- a/scripts/Utilities/RemoveUnused.py +++ b/scripts/Utilities/RemoveUnused.py @@ -7,10 +7,10 @@ # License: Contextual Copyleft AI (CCAI) License v1.0. # Full license in LICENSE file. # -# Unclear what license this tool should fall under. Assumed AGPL v3 +# Unclear what license this tool should fall under. Assumed CCAI # due to no code being taken from alex6H just inspiration, but to be # safe this tool shouldn't be used for commercial purposes to comply -# with the original tool's CC-SA-NA license. TODO: figure out if this is true +# with the original tool's CC-SA-NA license. See GPL/CC compatibility. # -------------------------------------------------------------------------------- import arcpy From 27ffb0e3a99dc7a60c6b88c57b2d5d0c41f0b177 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 22 Jun 2026 14:22:54 -0400 Subject: [PATCH 05/42] driveby: update comments --- scripts/FluvialGeomorphology/PolygonCenterline.py | 3 +++ scripts/FluvialGeomorphology/StreamNetwork.py | 6 +++--- scripts/TerrainAnalysis/LandscapePosition.py | 1 - scripts/TerrainModification/BurnCulverts.py | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/FluvialGeomorphology/PolygonCenterline.py b/scripts/FluvialGeomorphology/PolygonCenterline.py index bfe4ebd..1792da7 100644 --- a/scripts/FluvialGeomorphology/PolygonCenterline.py +++ b/scripts/FluvialGeomorphology/PolygonCenterline.py @@ -85,6 +85,9 @@ def getParameterInfo(self): # TODO: fill holes option + # TODO: one centerline per polygon option using convex hull on polygon + # https://github.com/bcmertz/SWCD-Tools/issues/161 + param2 = arcpy.Parameter( displayName="Output Features", name="out_features", diff --git a/scripts/FluvialGeomorphology/StreamNetwork.py b/scripts/FluvialGeomorphology/StreamNetwork.py index 053721b..c1caacf 100644 --- a/scripts/FluvialGeomorphology/StreamNetwork.py +++ b/scripts/FluvialGeomorphology/StreamNetwork.py @@ -181,7 +181,7 @@ def execute(self, parameters, messages): extent = parameters[1].value # parameters[2] is just a toggle for updateParameters to visualize what the user is doing stream = parameters[3].value - threshold_size, threshold_unit = parameters[4].valueAsText.split(" ") if parameters[4].value is not None else (None, None) # TODO: verify (None, None) is correct + threshold_size, threshold_unit = parameters[4].valueAsText.split(" ") if parameters[4].value is not None else (None, None) keep_fields = parameters[5].valueAsText.split(";") if parameters[5].value is not None else None # read in areal unit and map it's pretty string to the arcpy representation watershed_size_bool = parameters[6].value @@ -232,7 +232,7 @@ def execute(self, parameters, messages): stream_initiations_raster = arcpy.sa.SnapPourPoint( in_pour_point_data=scratch_end_points, in_accumulation_raster=flow_accumulation, - snap_distance=snap_dist, # TODO: choose reasonable distance to look for initiation points + snap_distance=snap_dist, # TODO: choose reasonable distance to look for initiation points, user option? ) # convert stream initiation raster to points @@ -270,7 +270,7 @@ def execute(self, parameters, messages): join_type="KEEP_ALL", field_mapping=field_mapping, match_option="CLOSEST", - search_radius="25 Meters", # TODO: consider non-hardcoded alternative + search_radius="25 Meters", # TODO: consider non-hardcoded alternative, user option same as above? ) # remove `Join_Count` and `TARGET_FID` fields diff --git a/scripts/TerrainAnalysis/LandscapePosition.py b/scripts/TerrainAnalysis/LandscapePosition.py index 04678b5..774b95b 100644 --- a/scripts/TerrainAnalysis/LandscapePosition.py +++ b/scripts/TerrainAnalysis/LandscapePosition.py @@ -145,7 +145,6 @@ def execute(self, parameters, messages): # slope # TODO: change resolution to a different scale, Deumlich did 125m - # TODO: would an arcpy.ia.RasterCollection help here? log("calculating slope") slope = arcpy.sa.Slope(dem, "DEGREE", "", "GEODESIC", z_unit) diff --git a/scripts/TerrainModification/BurnCulverts.py b/scripts/TerrainModification/BurnCulverts.py index 614411d..1845779 100644 --- a/scripts/TerrainModification/BurnCulverts.py +++ b/scripts/TerrainModification/BurnCulverts.py @@ -162,6 +162,8 @@ def execute(self, parameters, messages): # NOTE: can't use builtin pointtoline because we only have two points per line :( lines = [] with arcpy.da.SearchCursor(scratch_points_merge, ["SHAPE@XY", "grid_code"], sql_clause=(None, "ORDER BY grid_code")) as points: + # store points as {grid_code: [point]} and lines as {grid_code: arcpy.Polyline} + # not super elegant data structure and could be done better with two separate data structures with static types point_dict = {} for point in points: x, y = point[0] @@ -194,13 +196,12 @@ def execute(self, parameters, messages): # set elev to 0 with arcpy.da.UpdateCursor(scratch_stream_buffer, [elevation_field]) as cursor: for point in cursor: - point[0] = 0 # TODO: find way to get the elevation value without a fill - downstream elev? + point[0] = 0 # TODO: find way to get the elevation value if the user doesn't fill output (use downstream elev? fill locally?) cursor.updateRow(point) # polygon to raster arcpy.conversion.PolygonToRaster(scratch_stream_buffer,elevation_field,scratch_burned_raster, cellsize=1) - # fill output raster if fill_depressions: # mosaic to new raster From 9a58a2b8f6bc828315d7cde0bc7f1b080bc5ad95 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 22 Jun 2026 18:24:54 -0400 Subject: [PATCH 06/42] mvp --- SWCD Tools.PolygonCenterline.pyt.xml | 2 + .../FluvialGeomorphology/PolygonCenterline.py | 186 +++++++++++++----- scripts/FluvialGeomorphology/__init__.py | 2 +- 3 files changed, 145 insertions(+), 45 deletions(-) create mode 100644 SWCD Tools.PolygonCenterline.pyt.xml diff --git a/SWCD Tools.PolygonCenterline.pyt.xml b/SWCD Tools.PolygonCenterline.pyt.xml new file mode 100644 index 0000000..3453cfd --- /dev/null +++ b/SWCD Tools.PolygonCenterline.pyt.xml @@ -0,0 +1,2 @@ + +20260622171152001.0TRUE diff --git a/scripts/FluvialGeomorphology/PolygonCenterline.py b/scripts/FluvialGeomorphology/PolygonCenterline.py index 1792da7..14a1894 100644 --- a/scripts/FluvialGeomorphology/PolygonCenterline.py +++ b/scripts/FluvialGeomorphology/PolygonCenterline.py @@ -16,47 +16,6 @@ from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate -def polygon_centerline(polygon, edge_points): - """Find centerline of polygon.""" - - # TODO: densify - arcpy.edit.Densify( - in_features="vbet_SimplifyPolygon1", - densification_method="DISTANCE", - distance="50 Meters", - max_deviation="0.1 Meters", - max_angle=10, - max_vertex_per_segment=None - ) - - # TODO: thiessen - - # TODO: polygon to line - - # TODO: select by location: completely within - - # TODO: Export to new fc - - # TODO: Dissolve (single, unsplit) - - # TODO: feat vert to pts (dangling) - - # TODO: Selct by location (itnersect pt and line) - - # TODO: Delete selected - - # TODO: Generate near table - - # TODO: XY to line from table - - # TODO: Merge line with centerline - - # TODO: Select by location (completely within VBET) - - # TODO: Delete selected - - return centerline - class PolygonCenterline(object): def __init__(self): """Define the tool (tool name is the name of the class).""" @@ -74,14 +33,17 @@ def getParameterInfo(self): parameterType="Required", direction="Input") param0.filter.list = ["Polygon"] + param0.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows polygon creation param1 = arcpy.Parameter( displayName="Connecting Edge Points", name="points", datatype="GPFeatureLayer", parameterType="Optional", + multiValue=True, direction="Input") param1.filter.list = ["Point"] + param1.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows polygon creation # TODO: fill holes option @@ -126,9 +88,145 @@ def execute(self, parameters, messages): # TODO: handle multiple polygons / points - # find centerline - centerline = polygon_centerline(polygon, edge_points) - arcpy.conversion.ExportFeatures(centerline, output_file) + # create scratch layers + scratch_polygon = arcpy.CreateScratchName("polygon", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + scratch_vertices = arcpy.CreateScratchName("vertices", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + scratch_thiessen = arcpy.CreateScratchName("thiessen", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + scratch_line = arcpy.CreateScratchName("line", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + scratch_dissolve = arcpy.CreateScratchName("dissolve", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + scratch_dangling = arcpy.CreateScratchName("dangling", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + + # export polygon to scratch + log("export") + arcpy.management.CopyFeatures(polygon, scratch_polygon) + + # densify + log("densify") + arcpy.edit.Densify( + in_features=scratch_polygon, + densification_method="DISTANCE", + distance="50 Meters", + max_deviation="0.1 Meters", + ) + + # polygon to vertices + log("feature vertices to points") + arcpy.management.FeatureVerticesToPoints( + in_features=scratch_polygon, + out_feature_class=scratch_vertices, + point_location="ALL" + ) + + # thiessen + log("thiessen") + arcpy.analysis.CreateThiessenPolygons( + in_features=scratch_vertices, + out_feature_class=scratch_thiessen, + fields_to_copy="ONLY_FID" + ) + + # polygon to line + log("polygon to line") + arcpy.management.PolygonToLine( + in_features=scratch_thiessen, + out_feature_class=scratch_line, + neighbor_option="IDENTIFY_NEIGHBORS" + ) + + # select by location: completely within + log("select by location") + completely_within, _, _ = arcpy.management.SelectLayerByLocation( + in_layer=scratch_line, + overlap_type="COMPLETELY_WITHIN", + select_features=polygon, + search_distance=None, + selection_type="NEW_SELECTION", + invert_spatial_relationship="NOT_INVERT" + ) + + # dissolve (single, unsplit) + log("dissolve") + arcpy.management.Dissolve( + in_features=completely_within, + out_feature_class=scratch_dissolve, + multi_part="SINGLE_PART", + unsplit_lines="UNSPLIT_LINES", + ) + + # feat vert to pts (dangling) + log("dangling points") + arcpy.management.FeatureVerticesToPoints( + in_features=scratch_dissolve, + out_feature_class=scratch_dangling, + point_location="DANGLE" + ) + + # Selct by location (itnersect pt and line) + log("select dangling") + dangling, _, _ = arcpy.management.SelectLayerByLocation( + in_layer=scratch_dissolve, + overlap_type="INTERSECT", + select_features=scratch_dangling, + search_distance=None, + selection_type="NEW_SELECTION", + invert_spatial_relationship="NOT_INVERT" + ) + + # Delete selected + log("delete selected") + arcpy.management.DeleteFeatures(dangling) + + # dissolve (single, unsplit) + log("dissolve to single part") + arcpy.management.Dissolve( + in_features=dangling, + out_feature_class=output_file, + dissolve_field=None, + multi_part="MULTI_PART", + unsplit_lines="DISSOLVE_LINES", + ) + + ### IF CONNECTION POINTS ### + + # # TODO: Generate near table + # arcpy.analysis.GenerateNearTable( + # in_features="PolygonCenterlinePolygonPolygons_PolygonCenterline4", + # near_features="'Polygon Centerline Connecting Edge Points (Points) 2'", + # out_table=r"G:\GIS\Reya\tmp\MyPraaoject\MyPraaoject.gdb\PolygonCenterl_GenerateNearT", + # search_radius=None, + # location="LOCATION", + # angle="NO_ANGLE", + # closest="CLOSEST", + # closest_count=0, + # method="PLANAR", + # distance_unit="" + # ) + + # # TODO: XY to line from table\ + # arcpy.management.XYToLine( + # in_table="PolygonCenterl_GenerateNearT", + # out_featureclass=r"G:\GIS\Reya\tmp\MyPraaoject\MyPraaoject.gdb\PolygonCenterl_Gene_XYToLine", + # startx_field="FROM_X", + # starty_field="FROM_Y", + # endx_field="NEAR_X", + # endy_field="NEAR_Y", + # line_type="GEODESIC", + # id_field="IN_FID", + # spatial_reference='PROJCS["WGS_1984_UTM_Zone_18N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-75.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]];-5120900 -9998100 10000;-100000 10000;-100000 10000;0.001;0.001;0.001;IsHighPrecision', + # attributes="NO_ATTRIBUTES" + # ) + + # TODO: Merge line with centerline + + # TODO: feature vertices to points dangling + + # TODO: dissolve (multi-part, dissolve) + + # TODO: Select by location (touching dangling) + + # TODO: Select by location (completely within VBET) + + # TODO: Delete selected # add data log("adding data") diff --git a/scripts/FluvialGeomorphology/__init__.py b/scripts/FluvialGeomorphology/__init__.py index 1403e98..4dc5b5d 100644 --- a/scripts/FluvialGeomorphology/__init__.py +++ b/scripts/FluvialGeomorphology/__init__.py @@ -11,4 +11,4 @@ from .StreamNetwork import StreamNetwork from .StreambankDetection import StreambankDetection from .GenerateCrossSections import GenerateCrossSections, generate_transects, transect_line -from .PolygonCenterline import PolygonCenterline, polygon_centerline +from .PolygonCenterline import PolygonCenterline From 202cde145db780e06125cd975cf7bb7385045dd1 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 23 Jun 2026 18:20:59 -0400 Subject: [PATCH 07/42] feature class to numpy array and delaunay triangulation --- scripts/helpers/__init__.py | 1 + scripts/helpers/geometry.py | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 scripts/helpers/geometry.py diff --git a/scripts/helpers/__init__.py b/scripts/helpers/__init__.py index e819d8d..2e3747e 100644 --- a/scripts/helpers/__init__.py +++ b/scripts/helpers/__init__.py @@ -13,3 +13,4 @@ from .tool import license, setup_environment, reload_module, empty_workspace from .units import get_z_unit, get_linear_unit, Z_UNITS, LINEAR_UNITS, AREAL_UNITS, \ LINEAR_UNITS_MAP, AREAL_UNITS_MAP, SPATIAL_TO_LINEAR, LINEAR_TO_AREAL, convert_area, convert_length +from .geometry import fc_to_numpy_array, delaunay diff --git a/scripts/helpers/geometry.py b/scripts/helpers/geometry.py new file mode 100644 index 0000000..81bb8ea --- /dev/null +++ b/scripts/helpers/geometry.py @@ -0,0 +1,52 @@ +# --------------------------------------------------------------------------------- +# Name: Geometry Helper +# Purpose: This package contains various helpers and tools for analyzing geometry. +# +# License: Contextual Copyleft AI (CCAI) License v1.0. +# Full license in LICENSE file. +# --------------------------------------------------------------------------------- + + +import arcpy +import numpy as np +from scipy.spatial import Delaunay +from numpy.lib.recfunctions import structured_to_unstructured as stu + +# modified from https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/npg/npg_arc_npg.py#L639 +def fc_to_numpy_array(in_fc): + """Get the geometry from a feature class and clean it up into a numpy.ndarray. + Returns either a structured or unstructured numpy.ndarray. + """ + arr = arcpy.da.FeatureClassToNumPyArray( + in_table=in_fc, + field_names=['SHAPE@X', 'SHAPE@Y'], + explode_to_points=True + ) + x, y = [arr[name] for name in ['SHAPE@X', 'SHAPE@Y']] + a = np.empty((len(x), ), dtype=np.dtype([('X', np.float64), ('Y', np.float64)])) + # round `X` and `Y` values + a['X'] = np.round(x, 3) + a['Y'] = np.round(y, 3) + xy = stu(a) + return a, xy + +def delaunay(in_fc, out_fc): + """Calculate the Delaunay triangulation of an input feature class' vertices.""" + spatial_ref = arcpy.Describe(in_fc).spatialReference + _, np_arr = fc_to_numpy_array(in_fc) + delaunay = Delaunay(np_arr).simplices + + # construct output polygons + features = [] + for tri in delaunay: + pts = [np_arr[idx] for idx in tri] # list of pt coords [[x1, y1], [x2, y2], ] + features.append(arcpy.Polygon(arcpy.Array([arcpy.Point(*pt) for pt in pts]), spatial_reference=spatial_ref)) + + # create output fc from polygons + arcpy.management.CopyFeatures(features, out_fc) + + return + +# TODO: consider manual voronoi polygon calculation +# https://gist.github.com/letmaik/8803860 +# https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/tbx_tools.py From d234efd6623a7113e33f9d258679d3fe0abcf3ea Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 24 Jun 2026 16:55:28 -0400 Subject: [PATCH 08/42] s --- scripts/helpers/geometry.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/helpers/geometry.py b/scripts/helpers/geometry.py index 81bb8ea..027cd1d 100644 --- a/scripts/helpers/geometry.py +++ b/scripts/helpers/geometry.py @@ -47,6 +47,23 @@ def delaunay(in_fc, out_fc): return -# TODO: consider manual voronoi polygon calculation +# voronoi polygon calculation # https://gist.github.com/letmaik/8803860 # https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/tbx_tools.py +def voronoi(delaunay_fc, out_fc): + spatial_ref = arcpy.Describe(delaunay_fc).spatialReference + _, np_arr = fc_to_numpy_array(delaunay_fc) + + + +# from https://stackoverflow.com/questions/10650645/python-calculate-voronoi-tesselation-from-scipys-delaunay-triangulation-in-3d/15783581#15783581 +def triangle_csc(pts): + rows, cols = pts.shape + + A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))], + [np.ones((1, rows)), np.zeros((1, 1))]]) + + b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1)))) + x = np.linalg.solve(A,b) + bary_coords = x[:-1] + return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0) From e604863a6e41c8d1f798105e0c6ef670f7450238 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 24 Jun 2026 18:17:28 -0400 Subject: [PATCH 09/42] rename scripts/ to src/ --- {scripts => src/scripts}/AgAssessment/Agland.py | 0 {scripts => src/scripts}/AgAssessment/DefineParcels.py | 0 {scripts => src/scripts}/AgAssessment/Export.py | 0 {scripts => src/scripts}/AgAssessment/Forest.py | 0 {scripts => src/scripts}/AgAssessment/NonAg.py | 0 {scripts => src/scripts}/AgAssessment/Process.py | 0 {scripts => src/scripts}/AgAssessment/Restart.py | 0 {scripts => src/scripts}/AgAssessment/__init__.py | 0 {scripts => src/scripts}/BufferTools/BufferPotential.py | 0 {scripts => src/scripts}/BufferTools/PointPlots.py | 0 {scripts => src/scripts}/BufferTools/ShrubClusters.py | 0 {scripts => src/scripts}/BufferTools/__init__.py | 0 .../scripts}/FluvialGeomorphology/GenerateCrossSections.py | 0 .../scripts}/FluvialGeomorphology/PolygonCenterline.py | 0 .../scripts}/FluvialGeomorphology/StreamCenterlineAdjuster.py | 0 {scripts => src/scripts}/FluvialGeomorphology/StreamElevation.py | 0 {scripts => src/scripts}/FluvialGeomorphology/StreamNetwork.py | 0 .../scripts}/FluvialGeomorphology/StreambankDetection.py | 0 {scripts => src/scripts}/FluvialGeomorphology/__init__.py | 0 {scripts => src/scripts}/Hydrology/CalculateEFH2.py | 0 {scripts => src/scripts}/Hydrology/RunoffCurveNumber.py | 0 {scripts => src/scripts}/Hydrology/SubBasinDelineation.py | 0 {scripts => src/scripts}/Hydrology/WatershedDelineation.py | 0 {scripts => src/scripts}/Hydrology/WatershedSize.py | 0 {scripts => src/scripts}/Hydrology/__init__.py | 0 {scripts => src/scripts}/TerrainAnalysis/LandscapePosition.py | 0 {scripts => src/scripts}/TerrainAnalysis/PotentialWetlands.py | 0 {scripts => src/scripts}/TerrainAnalysis/REMCalculator.py | 0 {scripts => src/scripts}/TerrainAnalysis/StreamPowerIndex.py | 0 .../scripts}/TerrainAnalysis/TopographicPositionIndex.py | 0 {scripts => src/scripts}/TerrainAnalysis/TopographicWetness.py | 0 {scripts => src/scripts}/TerrainAnalysis/VBET.py | 0 {scripts => src/scripts}/TerrainAnalysis/__init__.py | 0 {scripts => src/scripts}/TerrainModification/BermAnalysis.py | 0 {scripts => src/scripts}/TerrainModification/BurnCulverts.py | 0 {scripts => src/scripts}/TerrainModification/DamRemoval.py | 0 {scripts => src/scripts}/TerrainModification/__init__.py | 0 {scripts => src/scripts}/TileDrainage/DecisionTree.py | 0 {scripts => src/scripts}/TileDrainage/ImageDifferencing.py | 0 {scripts => src/scripts}/TileDrainage/ImageDifferencingClouds.py | 0 {scripts => src/scripts}/TileDrainage/ImageDifferencingSetup.py | 0 {scripts => src/scripts}/TileDrainage/__init__.py | 0 {scripts => src/scripts}/Utilities/CollectHistoricalRasters.py | 0 {scripts => src/scripts}/Utilities/ContourPolygon.py | 0 {scripts => src/scripts}/Utilities/ExportLayouts.py | 0 {scripts => src/scripts}/Utilities/GeocodeAddress.py | 0 {scripts => src/scripts}/Utilities/LocalMinimums.py | 0 {scripts => src/scripts}/Utilities/RemoveUnused.py | 0 {scripts => src/scripts}/Utilities/SlopePolygon.py | 0 {scripts => src/scripts}/Utilities/__init__.py | 0 {scripts => src/scripts}/__init__.py | 0 {scripts => src/scripts}/helpers/__init__.py | 0 {scripts => src/scripts}/helpers/geometry.py | 0 {scripts => src/scripts}/helpers/layers.py | 0 {scripts => src/scripts}/helpers/logging.py | 0 {scripts => src/scripts}/helpers/parameter.py | 0 {scripts => src/scripts}/helpers/rasters.py | 0 {scripts => src/scripts}/helpers/tool.py | 0 {scripts => src/scripts}/helpers/units.py | 0 59 files changed, 0 insertions(+), 0 deletions(-) rename {scripts => src/scripts}/AgAssessment/Agland.py (100%) rename {scripts => src/scripts}/AgAssessment/DefineParcels.py (100%) rename {scripts => src/scripts}/AgAssessment/Export.py (100%) rename {scripts => src/scripts}/AgAssessment/Forest.py (100%) rename {scripts => src/scripts}/AgAssessment/NonAg.py (100%) rename {scripts => src/scripts}/AgAssessment/Process.py (100%) rename {scripts => src/scripts}/AgAssessment/Restart.py (100%) rename {scripts => src/scripts}/AgAssessment/__init__.py (100%) rename {scripts => src/scripts}/BufferTools/BufferPotential.py (100%) rename {scripts => src/scripts}/BufferTools/PointPlots.py (100%) rename {scripts => src/scripts}/BufferTools/ShrubClusters.py (100%) rename {scripts => src/scripts}/BufferTools/__init__.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/GenerateCrossSections.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/PolygonCenterline.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/StreamCenterlineAdjuster.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/StreamElevation.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/StreamNetwork.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/StreambankDetection.py (100%) rename {scripts => src/scripts}/FluvialGeomorphology/__init__.py (100%) rename {scripts => src/scripts}/Hydrology/CalculateEFH2.py (100%) rename {scripts => src/scripts}/Hydrology/RunoffCurveNumber.py (100%) rename {scripts => src/scripts}/Hydrology/SubBasinDelineation.py (100%) rename {scripts => src/scripts}/Hydrology/WatershedDelineation.py (100%) rename {scripts => src/scripts}/Hydrology/WatershedSize.py (100%) rename {scripts => src/scripts}/Hydrology/__init__.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/LandscapePosition.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/PotentialWetlands.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/REMCalculator.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/StreamPowerIndex.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/TopographicPositionIndex.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/TopographicWetness.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/VBET.py (100%) rename {scripts => src/scripts}/TerrainAnalysis/__init__.py (100%) rename {scripts => src/scripts}/TerrainModification/BermAnalysis.py (100%) rename {scripts => src/scripts}/TerrainModification/BurnCulverts.py (100%) rename {scripts => src/scripts}/TerrainModification/DamRemoval.py (100%) rename {scripts => src/scripts}/TerrainModification/__init__.py (100%) rename {scripts => src/scripts}/TileDrainage/DecisionTree.py (100%) rename {scripts => src/scripts}/TileDrainage/ImageDifferencing.py (100%) rename {scripts => src/scripts}/TileDrainage/ImageDifferencingClouds.py (100%) rename {scripts => src/scripts}/TileDrainage/ImageDifferencingSetup.py (100%) rename {scripts => src/scripts}/TileDrainage/__init__.py (100%) rename {scripts => src/scripts}/Utilities/CollectHistoricalRasters.py (100%) rename {scripts => src/scripts}/Utilities/ContourPolygon.py (100%) rename {scripts => src/scripts}/Utilities/ExportLayouts.py (100%) rename {scripts => src/scripts}/Utilities/GeocodeAddress.py (100%) rename {scripts => src/scripts}/Utilities/LocalMinimums.py (100%) rename {scripts => src/scripts}/Utilities/RemoveUnused.py (100%) rename {scripts => src/scripts}/Utilities/SlopePolygon.py (100%) rename {scripts => src/scripts}/Utilities/__init__.py (100%) rename {scripts => src/scripts}/__init__.py (100%) rename {scripts => src/scripts}/helpers/__init__.py (100%) rename {scripts => src/scripts}/helpers/geometry.py (100%) rename {scripts => src/scripts}/helpers/layers.py (100%) rename {scripts => src/scripts}/helpers/logging.py (100%) rename {scripts => src/scripts}/helpers/parameter.py (100%) rename {scripts => src/scripts}/helpers/rasters.py (100%) rename {scripts => src/scripts}/helpers/tool.py (100%) rename {scripts => src/scripts}/helpers/units.py (100%) diff --git a/scripts/AgAssessment/Agland.py b/src/scripts/AgAssessment/Agland.py similarity index 100% rename from scripts/AgAssessment/Agland.py rename to src/scripts/AgAssessment/Agland.py diff --git a/scripts/AgAssessment/DefineParcels.py b/src/scripts/AgAssessment/DefineParcels.py similarity index 100% rename from scripts/AgAssessment/DefineParcels.py rename to src/scripts/AgAssessment/DefineParcels.py diff --git a/scripts/AgAssessment/Export.py b/src/scripts/AgAssessment/Export.py similarity index 100% rename from scripts/AgAssessment/Export.py rename to src/scripts/AgAssessment/Export.py diff --git a/scripts/AgAssessment/Forest.py b/src/scripts/AgAssessment/Forest.py similarity index 100% rename from scripts/AgAssessment/Forest.py rename to src/scripts/AgAssessment/Forest.py diff --git a/scripts/AgAssessment/NonAg.py b/src/scripts/AgAssessment/NonAg.py similarity index 100% rename from scripts/AgAssessment/NonAg.py rename to src/scripts/AgAssessment/NonAg.py diff --git a/scripts/AgAssessment/Process.py b/src/scripts/AgAssessment/Process.py similarity index 100% rename from scripts/AgAssessment/Process.py rename to src/scripts/AgAssessment/Process.py diff --git a/scripts/AgAssessment/Restart.py b/src/scripts/AgAssessment/Restart.py similarity index 100% rename from scripts/AgAssessment/Restart.py rename to src/scripts/AgAssessment/Restart.py diff --git a/scripts/AgAssessment/__init__.py b/src/scripts/AgAssessment/__init__.py similarity index 100% rename from scripts/AgAssessment/__init__.py rename to src/scripts/AgAssessment/__init__.py diff --git a/scripts/BufferTools/BufferPotential.py b/src/scripts/BufferTools/BufferPotential.py similarity index 100% rename from scripts/BufferTools/BufferPotential.py rename to src/scripts/BufferTools/BufferPotential.py diff --git a/scripts/BufferTools/PointPlots.py b/src/scripts/BufferTools/PointPlots.py similarity index 100% rename from scripts/BufferTools/PointPlots.py rename to src/scripts/BufferTools/PointPlots.py diff --git a/scripts/BufferTools/ShrubClusters.py b/src/scripts/BufferTools/ShrubClusters.py similarity index 100% rename from scripts/BufferTools/ShrubClusters.py rename to src/scripts/BufferTools/ShrubClusters.py diff --git a/scripts/BufferTools/__init__.py b/src/scripts/BufferTools/__init__.py similarity index 100% rename from scripts/BufferTools/__init__.py rename to src/scripts/BufferTools/__init__.py diff --git a/scripts/FluvialGeomorphology/GenerateCrossSections.py b/src/scripts/FluvialGeomorphology/GenerateCrossSections.py similarity index 100% rename from scripts/FluvialGeomorphology/GenerateCrossSections.py rename to src/scripts/FluvialGeomorphology/GenerateCrossSections.py diff --git a/scripts/FluvialGeomorphology/PolygonCenterline.py b/src/scripts/FluvialGeomorphology/PolygonCenterline.py similarity index 100% rename from scripts/FluvialGeomorphology/PolygonCenterline.py rename to src/scripts/FluvialGeomorphology/PolygonCenterline.py diff --git a/scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py b/src/scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py similarity index 100% rename from scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py rename to src/scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py diff --git a/scripts/FluvialGeomorphology/StreamElevation.py b/src/scripts/FluvialGeomorphology/StreamElevation.py similarity index 100% rename from scripts/FluvialGeomorphology/StreamElevation.py rename to src/scripts/FluvialGeomorphology/StreamElevation.py diff --git a/scripts/FluvialGeomorphology/StreamNetwork.py b/src/scripts/FluvialGeomorphology/StreamNetwork.py similarity index 100% rename from scripts/FluvialGeomorphology/StreamNetwork.py rename to src/scripts/FluvialGeomorphology/StreamNetwork.py diff --git a/scripts/FluvialGeomorphology/StreambankDetection.py b/src/scripts/FluvialGeomorphology/StreambankDetection.py similarity index 100% rename from scripts/FluvialGeomorphology/StreambankDetection.py rename to src/scripts/FluvialGeomorphology/StreambankDetection.py diff --git a/scripts/FluvialGeomorphology/__init__.py b/src/scripts/FluvialGeomorphology/__init__.py similarity index 100% rename from scripts/FluvialGeomorphology/__init__.py rename to src/scripts/FluvialGeomorphology/__init__.py diff --git a/scripts/Hydrology/CalculateEFH2.py b/src/scripts/Hydrology/CalculateEFH2.py similarity index 100% rename from scripts/Hydrology/CalculateEFH2.py rename to src/scripts/Hydrology/CalculateEFH2.py diff --git a/scripts/Hydrology/RunoffCurveNumber.py b/src/scripts/Hydrology/RunoffCurveNumber.py similarity index 100% rename from scripts/Hydrology/RunoffCurveNumber.py rename to src/scripts/Hydrology/RunoffCurveNumber.py diff --git a/scripts/Hydrology/SubBasinDelineation.py b/src/scripts/Hydrology/SubBasinDelineation.py similarity index 100% rename from scripts/Hydrology/SubBasinDelineation.py rename to src/scripts/Hydrology/SubBasinDelineation.py diff --git a/scripts/Hydrology/WatershedDelineation.py b/src/scripts/Hydrology/WatershedDelineation.py similarity index 100% rename from scripts/Hydrology/WatershedDelineation.py rename to src/scripts/Hydrology/WatershedDelineation.py diff --git a/scripts/Hydrology/WatershedSize.py b/src/scripts/Hydrology/WatershedSize.py similarity index 100% rename from scripts/Hydrology/WatershedSize.py rename to src/scripts/Hydrology/WatershedSize.py diff --git a/scripts/Hydrology/__init__.py b/src/scripts/Hydrology/__init__.py similarity index 100% rename from scripts/Hydrology/__init__.py rename to src/scripts/Hydrology/__init__.py diff --git a/scripts/TerrainAnalysis/LandscapePosition.py b/src/scripts/TerrainAnalysis/LandscapePosition.py similarity index 100% rename from scripts/TerrainAnalysis/LandscapePosition.py rename to src/scripts/TerrainAnalysis/LandscapePosition.py diff --git a/scripts/TerrainAnalysis/PotentialWetlands.py b/src/scripts/TerrainAnalysis/PotentialWetlands.py similarity index 100% rename from scripts/TerrainAnalysis/PotentialWetlands.py rename to src/scripts/TerrainAnalysis/PotentialWetlands.py diff --git a/scripts/TerrainAnalysis/REMCalculator.py b/src/scripts/TerrainAnalysis/REMCalculator.py similarity index 100% rename from scripts/TerrainAnalysis/REMCalculator.py rename to src/scripts/TerrainAnalysis/REMCalculator.py diff --git a/scripts/TerrainAnalysis/StreamPowerIndex.py b/src/scripts/TerrainAnalysis/StreamPowerIndex.py similarity index 100% rename from scripts/TerrainAnalysis/StreamPowerIndex.py rename to src/scripts/TerrainAnalysis/StreamPowerIndex.py diff --git a/scripts/TerrainAnalysis/TopographicPositionIndex.py b/src/scripts/TerrainAnalysis/TopographicPositionIndex.py similarity index 100% rename from scripts/TerrainAnalysis/TopographicPositionIndex.py rename to src/scripts/TerrainAnalysis/TopographicPositionIndex.py diff --git a/scripts/TerrainAnalysis/TopographicWetness.py b/src/scripts/TerrainAnalysis/TopographicWetness.py similarity index 100% rename from scripts/TerrainAnalysis/TopographicWetness.py rename to src/scripts/TerrainAnalysis/TopographicWetness.py diff --git a/scripts/TerrainAnalysis/VBET.py b/src/scripts/TerrainAnalysis/VBET.py similarity index 100% rename from scripts/TerrainAnalysis/VBET.py rename to src/scripts/TerrainAnalysis/VBET.py diff --git a/scripts/TerrainAnalysis/__init__.py b/src/scripts/TerrainAnalysis/__init__.py similarity index 100% rename from scripts/TerrainAnalysis/__init__.py rename to src/scripts/TerrainAnalysis/__init__.py diff --git a/scripts/TerrainModification/BermAnalysis.py b/src/scripts/TerrainModification/BermAnalysis.py similarity index 100% rename from scripts/TerrainModification/BermAnalysis.py rename to src/scripts/TerrainModification/BermAnalysis.py diff --git a/scripts/TerrainModification/BurnCulverts.py b/src/scripts/TerrainModification/BurnCulverts.py similarity index 100% rename from scripts/TerrainModification/BurnCulverts.py rename to src/scripts/TerrainModification/BurnCulverts.py diff --git a/scripts/TerrainModification/DamRemoval.py b/src/scripts/TerrainModification/DamRemoval.py similarity index 100% rename from scripts/TerrainModification/DamRemoval.py rename to src/scripts/TerrainModification/DamRemoval.py diff --git a/scripts/TerrainModification/__init__.py b/src/scripts/TerrainModification/__init__.py similarity index 100% rename from scripts/TerrainModification/__init__.py rename to src/scripts/TerrainModification/__init__.py diff --git a/scripts/TileDrainage/DecisionTree.py b/src/scripts/TileDrainage/DecisionTree.py similarity index 100% rename from scripts/TileDrainage/DecisionTree.py rename to src/scripts/TileDrainage/DecisionTree.py diff --git a/scripts/TileDrainage/ImageDifferencing.py b/src/scripts/TileDrainage/ImageDifferencing.py similarity index 100% rename from scripts/TileDrainage/ImageDifferencing.py rename to src/scripts/TileDrainage/ImageDifferencing.py diff --git a/scripts/TileDrainage/ImageDifferencingClouds.py b/src/scripts/TileDrainage/ImageDifferencingClouds.py similarity index 100% rename from scripts/TileDrainage/ImageDifferencingClouds.py rename to src/scripts/TileDrainage/ImageDifferencingClouds.py diff --git a/scripts/TileDrainage/ImageDifferencingSetup.py b/src/scripts/TileDrainage/ImageDifferencingSetup.py similarity index 100% rename from scripts/TileDrainage/ImageDifferencingSetup.py rename to src/scripts/TileDrainage/ImageDifferencingSetup.py diff --git a/scripts/TileDrainage/__init__.py b/src/scripts/TileDrainage/__init__.py similarity index 100% rename from scripts/TileDrainage/__init__.py rename to src/scripts/TileDrainage/__init__.py diff --git a/scripts/Utilities/CollectHistoricalRasters.py b/src/scripts/Utilities/CollectHistoricalRasters.py similarity index 100% rename from scripts/Utilities/CollectHistoricalRasters.py rename to src/scripts/Utilities/CollectHistoricalRasters.py diff --git a/scripts/Utilities/ContourPolygon.py b/src/scripts/Utilities/ContourPolygon.py similarity index 100% rename from scripts/Utilities/ContourPolygon.py rename to src/scripts/Utilities/ContourPolygon.py diff --git a/scripts/Utilities/ExportLayouts.py b/src/scripts/Utilities/ExportLayouts.py similarity index 100% rename from scripts/Utilities/ExportLayouts.py rename to src/scripts/Utilities/ExportLayouts.py diff --git a/scripts/Utilities/GeocodeAddress.py b/src/scripts/Utilities/GeocodeAddress.py similarity index 100% rename from scripts/Utilities/GeocodeAddress.py rename to src/scripts/Utilities/GeocodeAddress.py diff --git a/scripts/Utilities/LocalMinimums.py b/src/scripts/Utilities/LocalMinimums.py similarity index 100% rename from scripts/Utilities/LocalMinimums.py rename to src/scripts/Utilities/LocalMinimums.py diff --git a/scripts/Utilities/RemoveUnused.py b/src/scripts/Utilities/RemoveUnused.py similarity index 100% rename from scripts/Utilities/RemoveUnused.py rename to src/scripts/Utilities/RemoveUnused.py diff --git a/scripts/Utilities/SlopePolygon.py b/src/scripts/Utilities/SlopePolygon.py similarity index 100% rename from scripts/Utilities/SlopePolygon.py rename to src/scripts/Utilities/SlopePolygon.py diff --git a/scripts/Utilities/__init__.py b/src/scripts/Utilities/__init__.py similarity index 100% rename from scripts/Utilities/__init__.py rename to src/scripts/Utilities/__init__.py diff --git a/scripts/__init__.py b/src/scripts/__init__.py similarity index 100% rename from scripts/__init__.py rename to src/scripts/__init__.py diff --git a/scripts/helpers/__init__.py b/src/scripts/helpers/__init__.py similarity index 100% rename from scripts/helpers/__init__.py rename to src/scripts/helpers/__init__.py diff --git a/scripts/helpers/geometry.py b/src/scripts/helpers/geometry.py similarity index 100% rename from scripts/helpers/geometry.py rename to src/scripts/helpers/geometry.py diff --git a/scripts/helpers/layers.py b/src/scripts/helpers/layers.py similarity index 100% rename from scripts/helpers/layers.py rename to src/scripts/helpers/layers.py diff --git a/scripts/helpers/logging.py b/src/scripts/helpers/logging.py similarity index 100% rename from scripts/helpers/logging.py rename to src/scripts/helpers/logging.py diff --git a/scripts/helpers/parameter.py b/src/scripts/helpers/parameter.py similarity index 100% rename from scripts/helpers/parameter.py rename to src/scripts/helpers/parameter.py diff --git a/scripts/helpers/rasters.py b/src/scripts/helpers/rasters.py similarity index 100% rename from scripts/helpers/rasters.py rename to src/scripts/helpers/rasters.py diff --git a/scripts/helpers/tool.py b/src/scripts/helpers/tool.py similarity index 100% rename from scripts/helpers/tool.py rename to src/scripts/helpers/tool.py diff --git a/scripts/helpers/units.py b/src/scripts/helpers/units.py similarity index 100% rename from scripts/helpers/units.py rename to src/scripts/helpers/units.py From 8eb870ffcc0fcb1fc6eef92a457ee73504d73f72 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 24 Jun 2026 18:20:34 -0400 Subject: [PATCH 10/42] rename --- src/{scripts => }/AgAssessment/Agland.py | 0 src/{scripts => }/AgAssessment/DefineParcels.py | 0 src/{scripts => }/AgAssessment/Export.py | 0 src/{scripts => }/AgAssessment/Forest.py | 0 src/{scripts => }/AgAssessment/NonAg.py | 0 src/{scripts => }/AgAssessment/Process.py | 0 src/{scripts => }/AgAssessment/Restart.py | 0 src/{scripts => }/AgAssessment/__init__.py | 0 src/{scripts => }/BufferTools/BufferPotential.py | 0 src/{scripts => }/BufferTools/PointPlots.py | 0 src/{scripts => }/BufferTools/ShrubClusters.py | 0 src/{scripts => }/BufferTools/__init__.py | 0 src/{scripts => }/FluvialGeomorphology/GenerateCrossSections.py | 0 src/{scripts => }/FluvialGeomorphology/PolygonCenterline.py | 0 .../FluvialGeomorphology/StreamCenterlineAdjuster.py | 0 src/{scripts => }/FluvialGeomorphology/StreamElevation.py | 0 src/{scripts => }/FluvialGeomorphology/StreamNetwork.py | 0 src/{scripts => }/FluvialGeomorphology/StreambankDetection.py | 0 src/{scripts => }/FluvialGeomorphology/__init__.py | 0 src/{scripts => }/Hydrology/CalculateEFH2.py | 0 src/{scripts => }/Hydrology/RunoffCurveNumber.py | 0 src/{scripts => }/Hydrology/SubBasinDelineation.py | 0 src/{scripts => }/Hydrology/WatershedDelineation.py | 0 src/{scripts => }/Hydrology/WatershedSize.py | 0 src/{scripts => }/Hydrology/__init__.py | 0 src/{scripts => }/TerrainAnalysis/LandscapePosition.py | 0 src/{scripts => }/TerrainAnalysis/PotentialWetlands.py | 0 src/{scripts => }/TerrainAnalysis/REMCalculator.py | 0 src/{scripts => }/TerrainAnalysis/StreamPowerIndex.py | 0 src/{scripts => }/TerrainAnalysis/TopographicPositionIndex.py | 0 src/{scripts => }/TerrainAnalysis/TopographicWetness.py | 0 src/{scripts => }/TerrainAnalysis/VBET.py | 0 src/{scripts => }/TerrainAnalysis/__init__.py | 0 src/{scripts => }/TerrainModification/BermAnalysis.py | 0 src/{scripts => }/TerrainModification/BurnCulverts.py | 0 src/{scripts => }/TerrainModification/DamRemoval.py | 0 src/{scripts => }/TerrainModification/__init__.py | 0 src/{scripts => }/TileDrainage/DecisionTree.py | 0 src/{scripts => }/TileDrainage/ImageDifferencing.py | 0 src/{scripts => }/TileDrainage/ImageDifferencingClouds.py | 0 src/{scripts => }/TileDrainage/ImageDifferencingSetup.py | 0 src/{scripts => }/TileDrainage/__init__.py | 0 src/{scripts => }/Utilities/CollectHistoricalRasters.py | 0 src/{scripts => }/Utilities/ContourPolygon.py | 0 src/{scripts => }/Utilities/ExportLayouts.py | 0 src/{scripts => }/Utilities/GeocodeAddress.py | 0 src/{scripts => }/Utilities/LocalMinimums.py | 0 src/{scripts => }/Utilities/RemoveUnused.py | 0 src/{scripts => }/Utilities/SlopePolygon.py | 0 src/{scripts => }/Utilities/__init__.py | 0 src/{scripts => }/__init__.py | 0 src/{scripts => }/helpers/__init__.py | 0 src/{scripts => }/helpers/geometry.py | 0 src/{scripts => }/helpers/layers.py | 0 src/{scripts => }/helpers/logging.py | 0 src/{scripts => }/helpers/parameter.py | 0 src/{scripts => }/helpers/rasters.py | 0 src/{scripts => }/helpers/tool.py | 0 src/{scripts => }/helpers/units.py | 0 59 files changed, 0 insertions(+), 0 deletions(-) rename src/{scripts => }/AgAssessment/Agland.py (100%) rename src/{scripts => }/AgAssessment/DefineParcels.py (100%) rename src/{scripts => }/AgAssessment/Export.py (100%) rename src/{scripts => }/AgAssessment/Forest.py (100%) rename src/{scripts => }/AgAssessment/NonAg.py (100%) rename src/{scripts => }/AgAssessment/Process.py (100%) rename src/{scripts => }/AgAssessment/Restart.py (100%) rename src/{scripts => }/AgAssessment/__init__.py (100%) rename src/{scripts => }/BufferTools/BufferPotential.py (100%) rename src/{scripts => }/BufferTools/PointPlots.py (100%) rename src/{scripts => }/BufferTools/ShrubClusters.py (100%) rename src/{scripts => }/BufferTools/__init__.py (100%) rename src/{scripts => }/FluvialGeomorphology/GenerateCrossSections.py (100%) rename src/{scripts => }/FluvialGeomorphology/PolygonCenterline.py (100%) rename src/{scripts => }/FluvialGeomorphology/StreamCenterlineAdjuster.py (100%) rename src/{scripts => }/FluvialGeomorphology/StreamElevation.py (100%) rename src/{scripts => }/FluvialGeomorphology/StreamNetwork.py (100%) rename src/{scripts => }/FluvialGeomorphology/StreambankDetection.py (100%) rename src/{scripts => }/FluvialGeomorphology/__init__.py (100%) rename src/{scripts => }/Hydrology/CalculateEFH2.py (100%) rename src/{scripts => }/Hydrology/RunoffCurveNumber.py (100%) rename src/{scripts => }/Hydrology/SubBasinDelineation.py (100%) rename src/{scripts => }/Hydrology/WatershedDelineation.py (100%) rename src/{scripts => }/Hydrology/WatershedSize.py (100%) rename src/{scripts => }/Hydrology/__init__.py (100%) rename src/{scripts => }/TerrainAnalysis/LandscapePosition.py (100%) rename src/{scripts => }/TerrainAnalysis/PotentialWetlands.py (100%) rename src/{scripts => }/TerrainAnalysis/REMCalculator.py (100%) rename src/{scripts => }/TerrainAnalysis/StreamPowerIndex.py (100%) rename src/{scripts => }/TerrainAnalysis/TopographicPositionIndex.py (100%) rename src/{scripts => }/TerrainAnalysis/TopographicWetness.py (100%) rename src/{scripts => }/TerrainAnalysis/VBET.py (100%) rename src/{scripts => }/TerrainAnalysis/__init__.py (100%) rename src/{scripts => }/TerrainModification/BermAnalysis.py (100%) rename src/{scripts => }/TerrainModification/BurnCulverts.py (100%) rename src/{scripts => }/TerrainModification/DamRemoval.py (100%) rename src/{scripts => }/TerrainModification/__init__.py (100%) rename src/{scripts => }/TileDrainage/DecisionTree.py (100%) rename src/{scripts => }/TileDrainage/ImageDifferencing.py (100%) rename src/{scripts => }/TileDrainage/ImageDifferencingClouds.py (100%) rename src/{scripts => }/TileDrainage/ImageDifferencingSetup.py (100%) rename src/{scripts => }/TileDrainage/__init__.py (100%) rename src/{scripts => }/Utilities/CollectHistoricalRasters.py (100%) rename src/{scripts => }/Utilities/ContourPolygon.py (100%) rename src/{scripts => }/Utilities/ExportLayouts.py (100%) rename src/{scripts => }/Utilities/GeocodeAddress.py (100%) rename src/{scripts => }/Utilities/LocalMinimums.py (100%) rename src/{scripts => }/Utilities/RemoveUnused.py (100%) rename src/{scripts => }/Utilities/SlopePolygon.py (100%) rename src/{scripts => }/Utilities/__init__.py (100%) rename src/{scripts => }/__init__.py (100%) rename src/{scripts => }/helpers/__init__.py (100%) rename src/{scripts => }/helpers/geometry.py (100%) rename src/{scripts => }/helpers/layers.py (100%) rename src/{scripts => }/helpers/logging.py (100%) rename src/{scripts => }/helpers/parameter.py (100%) rename src/{scripts => }/helpers/rasters.py (100%) rename src/{scripts => }/helpers/tool.py (100%) rename src/{scripts => }/helpers/units.py (100%) diff --git a/src/scripts/AgAssessment/Agland.py b/src/AgAssessment/Agland.py similarity index 100% rename from src/scripts/AgAssessment/Agland.py rename to src/AgAssessment/Agland.py diff --git a/src/scripts/AgAssessment/DefineParcels.py b/src/AgAssessment/DefineParcels.py similarity index 100% rename from src/scripts/AgAssessment/DefineParcels.py rename to src/AgAssessment/DefineParcels.py diff --git a/src/scripts/AgAssessment/Export.py b/src/AgAssessment/Export.py similarity index 100% rename from src/scripts/AgAssessment/Export.py rename to src/AgAssessment/Export.py diff --git a/src/scripts/AgAssessment/Forest.py b/src/AgAssessment/Forest.py similarity index 100% rename from src/scripts/AgAssessment/Forest.py rename to src/AgAssessment/Forest.py diff --git a/src/scripts/AgAssessment/NonAg.py b/src/AgAssessment/NonAg.py similarity index 100% rename from src/scripts/AgAssessment/NonAg.py rename to src/AgAssessment/NonAg.py diff --git a/src/scripts/AgAssessment/Process.py b/src/AgAssessment/Process.py similarity index 100% rename from src/scripts/AgAssessment/Process.py rename to src/AgAssessment/Process.py diff --git a/src/scripts/AgAssessment/Restart.py b/src/AgAssessment/Restart.py similarity index 100% rename from src/scripts/AgAssessment/Restart.py rename to src/AgAssessment/Restart.py diff --git a/src/scripts/AgAssessment/__init__.py b/src/AgAssessment/__init__.py similarity index 100% rename from src/scripts/AgAssessment/__init__.py rename to src/AgAssessment/__init__.py diff --git a/src/scripts/BufferTools/BufferPotential.py b/src/BufferTools/BufferPotential.py similarity index 100% rename from src/scripts/BufferTools/BufferPotential.py rename to src/BufferTools/BufferPotential.py diff --git a/src/scripts/BufferTools/PointPlots.py b/src/BufferTools/PointPlots.py similarity index 100% rename from src/scripts/BufferTools/PointPlots.py rename to src/BufferTools/PointPlots.py diff --git a/src/scripts/BufferTools/ShrubClusters.py b/src/BufferTools/ShrubClusters.py similarity index 100% rename from src/scripts/BufferTools/ShrubClusters.py rename to src/BufferTools/ShrubClusters.py diff --git a/src/scripts/BufferTools/__init__.py b/src/BufferTools/__init__.py similarity index 100% rename from src/scripts/BufferTools/__init__.py rename to src/BufferTools/__init__.py diff --git a/src/scripts/FluvialGeomorphology/GenerateCrossSections.py b/src/FluvialGeomorphology/GenerateCrossSections.py similarity index 100% rename from src/scripts/FluvialGeomorphology/GenerateCrossSections.py rename to src/FluvialGeomorphology/GenerateCrossSections.py diff --git a/src/scripts/FluvialGeomorphology/PolygonCenterline.py b/src/FluvialGeomorphology/PolygonCenterline.py similarity index 100% rename from src/scripts/FluvialGeomorphology/PolygonCenterline.py rename to src/FluvialGeomorphology/PolygonCenterline.py diff --git a/src/scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py b/src/FluvialGeomorphology/StreamCenterlineAdjuster.py similarity index 100% rename from src/scripts/FluvialGeomorphology/StreamCenterlineAdjuster.py rename to src/FluvialGeomorphology/StreamCenterlineAdjuster.py diff --git a/src/scripts/FluvialGeomorphology/StreamElevation.py b/src/FluvialGeomorphology/StreamElevation.py similarity index 100% rename from src/scripts/FluvialGeomorphology/StreamElevation.py rename to src/FluvialGeomorphology/StreamElevation.py diff --git a/src/scripts/FluvialGeomorphology/StreamNetwork.py b/src/FluvialGeomorphology/StreamNetwork.py similarity index 100% rename from src/scripts/FluvialGeomorphology/StreamNetwork.py rename to src/FluvialGeomorphology/StreamNetwork.py diff --git a/src/scripts/FluvialGeomorphology/StreambankDetection.py b/src/FluvialGeomorphology/StreambankDetection.py similarity index 100% rename from src/scripts/FluvialGeomorphology/StreambankDetection.py rename to src/FluvialGeomorphology/StreambankDetection.py diff --git a/src/scripts/FluvialGeomorphology/__init__.py b/src/FluvialGeomorphology/__init__.py similarity index 100% rename from src/scripts/FluvialGeomorphology/__init__.py rename to src/FluvialGeomorphology/__init__.py diff --git a/src/scripts/Hydrology/CalculateEFH2.py b/src/Hydrology/CalculateEFH2.py similarity index 100% rename from src/scripts/Hydrology/CalculateEFH2.py rename to src/Hydrology/CalculateEFH2.py diff --git a/src/scripts/Hydrology/RunoffCurveNumber.py b/src/Hydrology/RunoffCurveNumber.py similarity index 100% rename from src/scripts/Hydrology/RunoffCurveNumber.py rename to src/Hydrology/RunoffCurveNumber.py diff --git a/src/scripts/Hydrology/SubBasinDelineation.py b/src/Hydrology/SubBasinDelineation.py similarity index 100% rename from src/scripts/Hydrology/SubBasinDelineation.py rename to src/Hydrology/SubBasinDelineation.py diff --git a/src/scripts/Hydrology/WatershedDelineation.py b/src/Hydrology/WatershedDelineation.py similarity index 100% rename from src/scripts/Hydrology/WatershedDelineation.py rename to src/Hydrology/WatershedDelineation.py diff --git a/src/scripts/Hydrology/WatershedSize.py b/src/Hydrology/WatershedSize.py similarity index 100% rename from src/scripts/Hydrology/WatershedSize.py rename to src/Hydrology/WatershedSize.py diff --git a/src/scripts/Hydrology/__init__.py b/src/Hydrology/__init__.py similarity index 100% rename from src/scripts/Hydrology/__init__.py rename to src/Hydrology/__init__.py diff --git a/src/scripts/TerrainAnalysis/LandscapePosition.py b/src/TerrainAnalysis/LandscapePosition.py similarity index 100% rename from src/scripts/TerrainAnalysis/LandscapePosition.py rename to src/TerrainAnalysis/LandscapePosition.py diff --git a/src/scripts/TerrainAnalysis/PotentialWetlands.py b/src/TerrainAnalysis/PotentialWetlands.py similarity index 100% rename from src/scripts/TerrainAnalysis/PotentialWetlands.py rename to src/TerrainAnalysis/PotentialWetlands.py diff --git a/src/scripts/TerrainAnalysis/REMCalculator.py b/src/TerrainAnalysis/REMCalculator.py similarity index 100% rename from src/scripts/TerrainAnalysis/REMCalculator.py rename to src/TerrainAnalysis/REMCalculator.py diff --git a/src/scripts/TerrainAnalysis/StreamPowerIndex.py b/src/TerrainAnalysis/StreamPowerIndex.py similarity index 100% rename from src/scripts/TerrainAnalysis/StreamPowerIndex.py rename to src/TerrainAnalysis/StreamPowerIndex.py diff --git a/src/scripts/TerrainAnalysis/TopographicPositionIndex.py b/src/TerrainAnalysis/TopographicPositionIndex.py similarity index 100% rename from src/scripts/TerrainAnalysis/TopographicPositionIndex.py rename to src/TerrainAnalysis/TopographicPositionIndex.py diff --git a/src/scripts/TerrainAnalysis/TopographicWetness.py b/src/TerrainAnalysis/TopographicWetness.py similarity index 100% rename from src/scripts/TerrainAnalysis/TopographicWetness.py rename to src/TerrainAnalysis/TopographicWetness.py diff --git a/src/scripts/TerrainAnalysis/VBET.py b/src/TerrainAnalysis/VBET.py similarity index 100% rename from src/scripts/TerrainAnalysis/VBET.py rename to src/TerrainAnalysis/VBET.py diff --git a/src/scripts/TerrainAnalysis/__init__.py b/src/TerrainAnalysis/__init__.py similarity index 100% rename from src/scripts/TerrainAnalysis/__init__.py rename to src/TerrainAnalysis/__init__.py diff --git a/src/scripts/TerrainModification/BermAnalysis.py b/src/TerrainModification/BermAnalysis.py similarity index 100% rename from src/scripts/TerrainModification/BermAnalysis.py rename to src/TerrainModification/BermAnalysis.py diff --git a/src/scripts/TerrainModification/BurnCulverts.py b/src/TerrainModification/BurnCulverts.py similarity index 100% rename from src/scripts/TerrainModification/BurnCulverts.py rename to src/TerrainModification/BurnCulverts.py diff --git a/src/scripts/TerrainModification/DamRemoval.py b/src/TerrainModification/DamRemoval.py similarity index 100% rename from src/scripts/TerrainModification/DamRemoval.py rename to src/TerrainModification/DamRemoval.py diff --git a/src/scripts/TerrainModification/__init__.py b/src/TerrainModification/__init__.py similarity index 100% rename from src/scripts/TerrainModification/__init__.py rename to src/TerrainModification/__init__.py diff --git a/src/scripts/TileDrainage/DecisionTree.py b/src/TileDrainage/DecisionTree.py similarity index 100% rename from src/scripts/TileDrainage/DecisionTree.py rename to src/TileDrainage/DecisionTree.py diff --git a/src/scripts/TileDrainage/ImageDifferencing.py b/src/TileDrainage/ImageDifferencing.py similarity index 100% rename from src/scripts/TileDrainage/ImageDifferencing.py rename to src/TileDrainage/ImageDifferencing.py diff --git a/src/scripts/TileDrainage/ImageDifferencingClouds.py b/src/TileDrainage/ImageDifferencingClouds.py similarity index 100% rename from src/scripts/TileDrainage/ImageDifferencingClouds.py rename to src/TileDrainage/ImageDifferencingClouds.py diff --git a/src/scripts/TileDrainage/ImageDifferencingSetup.py b/src/TileDrainage/ImageDifferencingSetup.py similarity index 100% rename from src/scripts/TileDrainage/ImageDifferencingSetup.py rename to src/TileDrainage/ImageDifferencingSetup.py diff --git a/src/scripts/TileDrainage/__init__.py b/src/TileDrainage/__init__.py similarity index 100% rename from src/scripts/TileDrainage/__init__.py rename to src/TileDrainage/__init__.py diff --git a/src/scripts/Utilities/CollectHistoricalRasters.py b/src/Utilities/CollectHistoricalRasters.py similarity index 100% rename from src/scripts/Utilities/CollectHistoricalRasters.py rename to src/Utilities/CollectHistoricalRasters.py diff --git a/src/scripts/Utilities/ContourPolygon.py b/src/Utilities/ContourPolygon.py similarity index 100% rename from src/scripts/Utilities/ContourPolygon.py rename to src/Utilities/ContourPolygon.py diff --git a/src/scripts/Utilities/ExportLayouts.py b/src/Utilities/ExportLayouts.py similarity index 100% rename from src/scripts/Utilities/ExportLayouts.py rename to src/Utilities/ExportLayouts.py diff --git a/src/scripts/Utilities/GeocodeAddress.py b/src/Utilities/GeocodeAddress.py similarity index 100% rename from src/scripts/Utilities/GeocodeAddress.py rename to src/Utilities/GeocodeAddress.py diff --git a/src/scripts/Utilities/LocalMinimums.py b/src/Utilities/LocalMinimums.py similarity index 100% rename from src/scripts/Utilities/LocalMinimums.py rename to src/Utilities/LocalMinimums.py diff --git a/src/scripts/Utilities/RemoveUnused.py b/src/Utilities/RemoveUnused.py similarity index 100% rename from src/scripts/Utilities/RemoveUnused.py rename to src/Utilities/RemoveUnused.py diff --git a/src/scripts/Utilities/SlopePolygon.py b/src/Utilities/SlopePolygon.py similarity index 100% rename from src/scripts/Utilities/SlopePolygon.py rename to src/Utilities/SlopePolygon.py diff --git a/src/scripts/Utilities/__init__.py b/src/Utilities/__init__.py similarity index 100% rename from src/scripts/Utilities/__init__.py rename to src/Utilities/__init__.py diff --git a/src/scripts/__init__.py b/src/__init__.py similarity index 100% rename from src/scripts/__init__.py rename to src/__init__.py diff --git a/src/scripts/helpers/__init__.py b/src/helpers/__init__.py similarity index 100% rename from src/scripts/helpers/__init__.py rename to src/helpers/__init__.py diff --git a/src/scripts/helpers/geometry.py b/src/helpers/geometry.py similarity index 100% rename from src/scripts/helpers/geometry.py rename to src/helpers/geometry.py diff --git a/src/scripts/helpers/layers.py b/src/helpers/layers.py similarity index 100% rename from src/scripts/helpers/layers.py rename to src/helpers/layers.py diff --git a/src/scripts/helpers/logging.py b/src/helpers/logging.py similarity index 100% rename from src/scripts/helpers/logging.py rename to src/helpers/logging.py diff --git a/src/scripts/helpers/parameter.py b/src/helpers/parameter.py similarity index 100% rename from src/scripts/helpers/parameter.py rename to src/helpers/parameter.py diff --git a/src/scripts/helpers/rasters.py b/src/helpers/rasters.py similarity index 100% rename from src/scripts/helpers/rasters.py rename to src/helpers/rasters.py diff --git a/src/scripts/helpers/tool.py b/src/helpers/tool.py similarity index 100% rename from src/scripts/helpers/tool.py rename to src/helpers/tool.py diff --git a/src/scripts/helpers/units.py b/src/helpers/units.py similarity index 100% rename from src/scripts/helpers/units.py rename to src/helpers/units.py From 350079e051f5bd27e84e9b09c0d593f83a9bf437 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 6 Jul 2026 10:11:37 -0400 Subject: [PATCH 11/42] bug: drive-by handle None unit --- src/FluvialGeomorphology/StreamNetwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FluvialGeomorphology/StreamNetwork.py b/src/FluvialGeomorphology/StreamNetwork.py index a791c1a..42070f4 100644 --- a/src/FluvialGeomorphology/StreamNetwork.py +++ b/src/FluvialGeomorphology/StreamNetwork.py @@ -185,7 +185,7 @@ def execute(self, parameters, messages): keep_fields = parameters[5].valueAsText.split(";") if parameters[5].value is not None else None # read in areal unit and map it's pretty string to the arcpy representation watershed_size_bool = parameters[6].value - watershed_size_unit = AREAL_UNITS_MAP[parameters[7].valueAsText] + watershed_size_unit = AREAL_UNITS_MAP[parameters[7].valueAsText] if parameters[7].value is not None else None output_file = parameters[8].valueAsText # set analysis extent From 1e277ae793b75bd3f5a5c2fc404c095f02290b73 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 6 Jul 2026 10:12:05 -0400 Subject: [PATCH 12/42] bug: separate strings --- src/FluvialGeomorphology/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FluvialGeomorphology/__init__.py b/src/FluvialGeomorphology/__init__.py index b2626c3..33707ae 100644 --- a/src/FluvialGeomorphology/__init__.py +++ b/src/FluvialGeomorphology/__init__.py @@ -22,7 +22,7 @@ "StreamElevation", "StreamNetwork", "StreambankDetection", - "PolygonCenterline" + "PolygonCenterline", "GenerateCrossSections", "generate_transects", "transect_line", From a64132c7184a06f0d0cdd7199893ef886b49b2d5 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 6 Jul 2026 12:15:33 -0400 Subject: [PATCH 13/42] bug: drive-by add warnings --- src/Utilities/GeocodeAddress.py | 4 ++-- src/Utilities/RemoveUnused.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Utilities/GeocodeAddress.py b/src/Utilities/GeocodeAddress.py index 2eb60e6..4f097cd 100644 --- a/src/Utilities/GeocodeAddress.py +++ b/src/Utilities/GeocodeAddress.py @@ -9,7 +9,7 @@ import os import arcpy -from ..helpers import license, reload_module, log +from ..helpers import license, reload_module, log, warn from ..helpers import setup_environment as setup class GeocodeAddress(object): @@ -78,7 +78,7 @@ def execute(self, parameters, messages): out_loc = None if len(geocoding_candidates) == 0: # return warning - arcpy.AddWarning("Warning: Couldn't find any matches for address '{}'".format(address)) + warn("Warning: Couldn't find any matches for address '{}'".format(address)) continue else: out_loc = geocoding_candidates[0] diff --git a/src/Utilities/RemoveUnused.py b/src/Utilities/RemoveUnused.py index 988b700..a1777d9 100644 --- a/src/Utilities/RemoveUnused.py +++ b/src/Utilities/RemoveUnused.py @@ -16,7 +16,7 @@ import arcpy import os -from ..helpers import license, reload_module, log +from ..helpers import license, reload_module, log, warn from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -100,7 +100,10 @@ def execute(self, parameters, messages): log("deleting unused data") for fc in unused: - arcpy.management.Delete(fc) + try: + arcpy.management.Delete(fc) + except: + warn("Could not delete {}".format(fc)) # save and exit program successfully log("saving project") From 43409416cbe7543399721577d34c615fecbe7f2a Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 7 Jul 2026 08:38:42 -0400 Subject: [PATCH 14/42] no bare exception and use warn where appropriate --- src/AgAssessment/Agland.py | 10 +++++----- src/AgAssessment/DefineParcels.py | 2 +- src/AgAssessment/Forest.py | 10 +++++----- src/AgAssessment/NonAg.py | 10 +++++----- src/AgAssessment/Process.py | 4 ++-- src/AgAssessment/Restart.py | 14 +++++++------- src/BufferTools/PointPlots.py | 6 +++--- src/Hydrology/SubBasinDelineation.py | 2 +- src/TerrainAnalysis/PotentialWetlands.py | 6 +++--- src/TerrainModification/BermAnalysis.py | 2 +- src/Utilities/CollectHistoricalRasters.py | 2 +- src/Utilities/RemoveUnused.py | 2 +- src/helpers/parameter.py | 2 +- src/helpers/rasters.py | 2 +- src/helpers/tool.py | 2 +- src/helpers/units.py | 2 +- 16 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/AgAssessment/Agland.py b/src/AgAssessment/Agland.py index fa94f15..5ac13a4 100644 --- a/src/AgAssessment/Agland.py +++ b/src/AgAssessment/Agland.py @@ -11,7 +11,7 @@ import arcpy from .DefineParcels import AG_ASSESSMENT_GDB_NAME -from ..helpers import license, reload_module, log, error +from ..helpers import license, reload_module, log, warn, error from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -64,16 +64,16 @@ def execute(self, parameters, messages): m = None try: m = project.listMaps(parcel)[0] - except: - log("unable to find map for {}, results may be incomplete".format(parcel)) + except Exception: + warn("unable to find map for {}, results may be incomplete".format(parcel)) continue # get parcel layer or drop off of map parcel_lyr = None try: parcel_lyr = m.listLayers("*_{}".format(parcel))[0] - except: - log("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) + except Exception: + warn("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) continue # check how many pieces are selected diff --git a/src/AgAssessment/DefineParcels.py b/src/AgAssessment/DefineParcels.py index fe61337..0643360 100644 --- a/src/AgAssessment/DefineParcels.py +++ b/src/AgAssessment/DefineParcels.py @@ -309,7 +309,7 @@ def execute(self, parameters, messages): # turn off parcel layer try: parcel_layer.visible = False - except: + except Exception: # parcel_layer is a shapefile pass diff --git a/src/AgAssessment/Forest.py b/src/AgAssessment/Forest.py index c2d2aa0..fb67f2c 100644 --- a/src/AgAssessment/Forest.py +++ b/src/AgAssessment/Forest.py @@ -11,7 +11,7 @@ import arcpy from .DefineParcels import AG_ASSESSMENT_GDB_NAME -from ..helpers import license, reload_module, log, error +from ..helpers import license, reload_module, log, warn, error from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -64,16 +64,16 @@ def execute(self, parameters, messages): m = None try: m = project.listMaps(parcel)[0] - except: - log("unable to find map for {}, results may be incomplete".format(parcel)) + except Exception: + warn("unable to find map for {}, results may be incomplete".format(parcel)) continue # get parcel layer or drop off of map parcel_lyr = None try: parcel_lyr = m.listLayers("*_{}".format(parcel))[0] - except: - log("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) + except Exception: + warn("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) continue # check how many pieces are selected diff --git a/src/AgAssessment/NonAg.py b/src/AgAssessment/NonAg.py index 2449857..a1b0730 100644 --- a/src/AgAssessment/NonAg.py +++ b/src/AgAssessment/NonAg.py @@ -11,7 +11,7 @@ import arcpy from .DefineParcels import AG_ASSESSMENT_GDB_NAME -from ..helpers import license, reload_module, log, error +from ..helpers import license, reload_module, log, warn, error from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -64,16 +64,16 @@ def execute(self, parameters, messages): m = None try: m = project.listMaps(parcel)[0] - except: - log("unable to find map for {}, results may be incomplete".format(parcel)) + except Exception: + warn("unable to find map for {}, results may be incomplete".format(parcel)) continue # get parcel layer or drop off of map parcel_lyr = None try: parcel_lyr = m.listLayers("*_{}".format(parcel))[0] - except: - log("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) + except Exception: + warn("no appropriate parcel layer found for {}, results may be incomplete".format(parcel)) continue # check how many pieces are selected diff --git a/src/AgAssessment/Process.py b/src/AgAssessment/Process.py index aaa6a49..487cf5e 100644 --- a/src/AgAssessment/Process.py +++ b/src/AgAssessment/Process.py @@ -139,7 +139,7 @@ def execute(self, parameters, messages): m = None try: m = project.listMaps(parcel)[0] - except: + except Exception: warn("unable to find map for {}, results may be incomplete".format(parcel)) continue @@ -151,7 +151,7 @@ def execute(self, parameters, messages): try: lyt = project.listLayouts(parcel)[0] layouts.append(lyt) - except: + except Exception: warn("couldn't find layout for parcel {}, results may be incomplete".format(parcel)) continue diff --git a/src/AgAssessment/Restart.py b/src/AgAssessment/Restart.py index 22e48c0..cf06da5 100644 --- a/src/AgAssessment/Restart.py +++ b/src/AgAssessment/Restart.py @@ -12,7 +12,7 @@ import arcpy from .DefineParcels import AG_ASSESSMENT_GDB_NAME -from ..helpers import license, reload_module, log, empty_workspace +from ..helpers import license, reload_module, log, warn, empty_workspace from ..helpers import setup_environment as setup class Restart(object): @@ -81,8 +81,8 @@ def execute(self, parameters, messages): with open(cache_file_path) as file: cache = json.load(file) parcels = cache["parcels"] - except: - log("Unable to find cache file and complete transaction. Please manually refresh the tool or manually clear out data.") + except Exception: + warn("Unable to find cache file and complete transaction. Please manually refresh the tool or manually clear out data.") return # clear out maps, layouts, and feature classes @@ -92,8 +92,8 @@ def execute(self, parameters, messages): lyt = None try: lyt = project.listLayouts(parcel)[0] - except: - log("couldn't find layout for parcel {}, results may be incomplete".format(parcel)) + except Exception: + warn("couldn't find layout for parcel {}, results may be incomplete".format(parcel)) continue # delete map @@ -103,8 +103,8 @@ def execute(self, parameters, messages): m = None try: m = project.listMaps(parcel)[0] - except: - log("unable to find map for parcel {}, results may be incomplete".format(parcel)) + except Exception: + warn("unable to find map for parcel {}, results may be incomplete".format(parcel)) continue # delete map diff --git a/src/BufferTools/PointPlots.py b/src/BufferTools/PointPlots.py index 94ab860..cd47e37 100644 --- a/src/BufferTools/PointPlots.py +++ b/src/BufferTools/PointPlots.py @@ -18,7 +18,7 @@ import arcpy import platform -from ..helpers import license, empty_workspace, set_required_parameter, reload_module, log +from ..helpers import license, empty_workspace, set_required_parameter, reload_module, log, warn from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -141,8 +141,8 @@ def execute(self, parameters, messages): log("create sampling locations") arcpy.management.CreateSpatialSamplingLocations(scratch_buffer, output_points, sampling_method="STRAT_POLY", strata_id_field=None, strata_count_method="PROP_AREA", num_samples=num, geometry_type="POINT", min_distance="{} Feet".format(radius*2)) - except: - log("Failed to create {} point plots with a radius of {} feet. It is likely because the buffer is too narrow to fit all of the point plots.".format(num, radius)) + except Exception: + warn("Failed to create {} point plots with a radius of {} feet. It is likely because the buffer is too narrow to fit all of the point plots.".format(num, radius)) radius = 11.8 num = int(math.ceil(acreage * 10)) diff --git a/src/Hydrology/SubBasinDelineation.py b/src/Hydrology/SubBasinDelineation.py index 3ce48f5..446e4be 100644 --- a/src/Hydrology/SubBasinDelineation.py +++ b/src/Hydrology/SubBasinDelineation.py @@ -80,7 +80,7 @@ def execute(self, parameters, messages): try: # find threshold in number of cells num_cells = cells_per_area(dem, threshold) - except: + except Exception: warn("failed to find raster linear unit, stream initiation threshold may be calculated incorrectly") # clip DEM raster to the watershed diff --git a/src/TerrainAnalysis/PotentialWetlands.py b/src/TerrainAnalysis/PotentialWetlands.py index 7f1c4a0..815fe5c 100644 --- a/src/TerrainAnalysis/PotentialWetlands.py +++ b/src/TerrainAnalysis/PotentialWetlands.py @@ -9,7 +9,7 @@ import arcpy -from ..helpers import license, get_oid, get_z_unit, empty_workspace, set_required_parameter, reload_module, log, raster_and_layer, Z_UNITS +from ..helpers import license, get_oid, get_z_unit, empty_workspace, set_required_parameter, reload_module, log, warn, raster_and_layer, Z_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -416,8 +416,8 @@ def execute(self, parameters, messages): sym.renderer.classificationField = field_name sym.renderer.colorRamp = project.listColorRamps('Blues (3 Classes)')[0] lyr.symbology = sym - except: - log("could not set output symbology properly") + except Exception: + warn("could not set output symbology properly") # cleanup log("deleting unneeded data") diff --git a/src/TerrainModification/BermAnalysis.py b/src/TerrainModification/BermAnalysis.py index 638ce8a..c0a3b80 100644 --- a/src/TerrainModification/BermAnalysis.py +++ b/src/TerrainModification/BermAnalysis.py @@ -257,7 +257,7 @@ def execute(self, parameters, messages): selection_tuple = tuple(selection_set) selection = "("+",".join([str(i) for i in selection_tuple])+")" expression = "{0} IN{1}".format(arcpy.AddFieldDelimiters(berms,oid_field),selection) - except: + except Exception: expression = "*" # iterate through berms diff --git a/src/Utilities/CollectHistoricalRasters.py b/src/Utilities/CollectHistoricalRasters.py index 49e3b7f..6b4fd74 100644 --- a/src/Utilities/CollectHistoricalRasters.py +++ b/src/Utilities/CollectHistoricalRasters.py @@ -77,7 +77,7 @@ def execute(self, parameters, messages): new_lyr_cim = new_lyr.getDefinition('V3') new_lyr_cim.expanded = False new_lyr.setDefinition(new_lyr_cim) - except: + except Exception: pass return diff --git a/src/Utilities/RemoveUnused.py b/src/Utilities/RemoveUnused.py index a1777d9..44cd645 100644 --- a/src/Utilities/RemoveUnused.py +++ b/src/Utilities/RemoveUnused.py @@ -102,7 +102,7 @@ def execute(self, parameters, messages): for fc in unused: try: arcpy.management.Delete(fc) - except: + except Exception: warn("Could not delete {}".format(fc)) # save and exit program successfully diff --git a/src/helpers/parameter.py b/src/helpers/parameter.py index 0cc289c..c3f9b42 100644 --- a/src/helpers/parameter.py +++ b/src/helpers/parameter.py @@ -39,7 +39,7 @@ def validate_spatial_reference(parameters): elif spatial_ref.type == "Geographic": valid_sr = False warning_message = warning_message_geographic - except: + except Exception: continue else: continue diff --git a/src/helpers/rasters.py b/src/helpers/rasters.py index 61d8a2c..17783a6 100644 --- a/src/helpers/rasters.py +++ b/src/helpers/rasters.py @@ -108,7 +108,7 @@ def min_cell_path(parameters) -> str: if min_cell_size is None or size_acres < min_cell_size: min_cell_size = size_acres min_cell_path = param.valueAsText - except: + except Exception: pass return min_cell_path diff --git a/src/helpers/tool.py b/src/helpers/tool.py index ca0e204..4d023b1 100644 --- a/src/helpers/tool.py +++ b/src/helpers/tool.py @@ -33,7 +33,7 @@ def license(licenses=[], version_required=""): if status != "Available": return False return True - except: + except Exception: return False diff --git a/src/helpers/units.py b/src/helpers/units.py index 636c2dd..b625bbc 100644 --- a/src/helpers/units.py +++ b/src/helpers/units.py @@ -24,7 +24,7 @@ def get_linear_unit(fc) -> str | None: try: desc = arcpy.Describe(fc) return desc.spatialReference.linearUnitName - except: + except Exception: return fc.spatialReference.linearUnitName # mapping of spatial reference linear unit to GPLinearUnit From da51bb454eddc0c557517de8bc38ebeff48c99e2 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 8 Jul 2026 17:01:57 -0400 Subject: [PATCH 15/42] working versions of delaunay triangulation, bounding box, and voronoi --- src/helpers/__init__.py | 15 +++++- src/helpers/geometry.py | 104 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py index ead832d..c5f0746 100644 --- a/src/helpers/__init__.py +++ b/src/helpers/__init__.py @@ -23,7 +23,14 @@ cells_per_length, ) from .tool import license, setup_environment, reload_module, empty_workspace -from .geometry import fc_to_numpy_array, delaunay +from .geometry import ( + fc_to_numpy_array, + bbox, + delaunay_fc, + voronoi, + voronoi_fc, + triangle_csc, +) from .units import ( get_z_unit, get_linear_unit, @@ -71,5 +78,9 @@ "convert_area", "convert_length", "fc_to_numpy_array", - "delaunay", + "bbox", + "delaunay_fc", + "voronoi", + "voronoi_fc", + "triangle_csc", ] diff --git a/src/helpers/geometry.py b/src/helpers/geometry.py index 027cd1d..fa6eba2 100644 --- a/src/helpers/geometry.py +++ b/src/helpers/geometry.py @@ -6,11 +6,11 @@ # Full license in LICENSE file. # --------------------------------------------------------------------------------- - import arcpy import numpy as np from scipy.spatial import Delaunay from numpy.lib.recfunctions import structured_to_unstructured as stu +from .logging import log # modified from https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/npg/npg_arc_npg.py#L639 def fc_to_numpy_array(in_fc): @@ -30,8 +30,27 @@ def fc_to_numpy_array(in_fc): xy = stu(a) return a, xy -def delaunay(in_fc, out_fc): - """Calculate the Delaunay triangulation of an input feature class' vertices.""" + +def bbox(arr): + """Find bounding box of polygon represented by numpy.ndarr `arr`""" + # rotate to get separate arrays of x and y coordinates + arr_rot = np.rot90(arr, axes=(0,1)) + + # flatten x and y coordinate arrays + x_arr = arr_rot[1].flatten() + y_arr = arr_rot[0].flatten() + + # find bounding box min + max + x_min = np.min(x_arr) + x_max = np.max(x_arr) + y_min = np.min(y_arr) + y_max = np.max(y_arr) + + return [[x_min, y_min],[x_max, y_min],[x_max, y_max],[x_min, y_max]] + + +def delaunay_fc(in_fc, out_fc): + """Calculate the Delaunay triangulation feature class from an input feature class' vertices.""" spatial_ref = arcpy.Describe(in_fc).spatialReference _, np_arr = fc_to_numpy_array(in_fc) delaunay = Delaunay(np_arr).simplices @@ -47,23 +66,88 @@ def delaunay(in_fc, out_fc): return + +def voronoi(delaunay): + """Calculate voronoi polygons from numpy array.""" + # calculate Delaunay triangulation + triangles = delaunay.points[delaunay.simplices] + + # find circumcenters of Delaunay triangulation + circum_centers = [triangle_csc(tri) for tri in triangles] + + # construct line segments between circumcenters + segments = [] + for i, triangle in enumerate(triangles): + circum_center = circum_centers[i] + if circum_center is None: + continue + for j, neighbor in enumerate(delaunay.neighbors[i]): + if neighbor != -1: + if circum_centers[neighbor] is None: + continue + segments.append((circum_center, circum_centers[neighbor])) + else: + ps = triangle[(j+1)%3] - triangle[(j-1)%3] + ps = np.array((ps[1], -ps[0])) + + middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5 + di = middle - triangle[j] + + ps /= np.linalg.norm(ps) + di /= np.linalg.norm(di) + + dot = np.dot(di, ps) + + if dot < 0.0: + ps *= -1000.0 + else: + ps *= 1000.0 + segments.append((circum_center, circum_center + ps)) + return segments + + # voronoi polygon calculation -# https://gist.github.com/letmaik/8803860 -# https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/tbx_tools.py -def voronoi(delaunay_fc, out_fc): +# +# modified from https://gist.github.com/letmaik/8803860 and +# https://stackoverflow.com/questions/10650645/python-calculate-voronoi-tesselation-from-scipys-delaunay-triangulation-in-3d/15783581#15783581 +# licensed under CC BY-SA 3.0 +def voronoi_fc(delaunay_fc, out_fc): + """Find voronoi polygon feature class from input.""" spatial_ref = arcpy.Describe(delaunay_fc).spatialReference _, np_arr = fc_to_numpy_array(delaunay_fc) + # calculate voronoi polygons numpy array + vor = voronoi(np_arr) + + # construct arcpy features + features = [] + for feature in vor: + array = arcpy.Array([arcpy.Point(*coords) for coords in feature]) + polyline = arcpy.Polyline(array, spatial_ref) + features.append(polyline) + + # create output fc from polygons + arcpy.management.CopyFeatures(features, out_fc) + return -# from https://stackoverflow.com/questions/10650645/python-calculate-voronoi-tesselation-from-scipys-delaunay-triangulation-in-3d/15783581#15783581 +# triangle circumcenter +# +# modified from https://stackoverflow.com/questions/10650645/python-calculate-voronoi-tesselation-from-scipys-delaunay-triangulation-in-3d/15783581#15783581 +# licensed under CC BY-SA 3.0 def triangle_csc(pts): - rows, cols = pts.shape + """Find circumcenter coordinates of triangle.""" + rows, _ = pts.shape A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))], [np.ones((1, rows)), np.zeros((1, 1))]]) b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1)))) - x = np.linalg.solve(A,b) + try: + x = np.linalg.solve(A,b) + except Exception: + return None bary_coords = x[:-1] - return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0) + sum = np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0) + + return sum From 4dbc168a737f8e00229387a9d913e7a2e70943fe Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 8 Jul 2026 18:28:17 -0400 Subject: [PATCH 16/42] wip - use helper geometry functions --- src/FluvialGeomorphology/PolygonCenterline.py | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/FluvialGeomorphology/PolygonCenterline.py b/src/FluvialGeomorphology/PolygonCenterline.py index 14a1894..1ddf0ff 100644 --- a/src/FluvialGeomorphology/PolygonCenterline.py +++ b/src/FluvialGeomorphology/PolygonCenterline.py @@ -11,8 +11,10 @@ import os import math import arcpy +import numpy as np +from scipy.spatial import Delaunay -from ..helpers import license, reload_module, log, empty_workspace +from ..helpers import license, reload_module, log, empty_workspace, fc_to_numpy_array, bbox, voronoi from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -33,17 +35,15 @@ def getParameterInfo(self): parameterType="Required", direction="Input") param0.filter.list = ["Polygon"] - param0.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows polygon creation param1 = arcpy.Parameter( displayName="Connecting Edge Points", name="points", datatype="GPFeatureLayer", parameterType="Optional", - multiValue=True, direction="Input") param1.filter.list = ["Point"] - param1.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows polygon creation + param1.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows point creation # TODO: fill holes option @@ -86,19 +86,67 @@ def execute(self, parameters, messages): edge_points = parameters[1].value output_file = parameters[2].valueAsText + # # create scratch layers + # log("create scratch layers") + # scratch_edge = arcpy.CreateScratchName("edge_pts", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + + # # export polygon to scratch + # log("export") + # arcpy.management.CopyFeatures(edge_points, scratch_edge) + + # convert fc to numpy array + log("converting input polygon feature class to numpy array") + spatial_ref = arcpy.Describe(polygon).spatialReference + _, np_arr = fc_to_numpy_array(polygon) + + # calculate bounding box and add it to the polygon numpy array + log("finding polygon bounding box") + box = bbox(np_arr) + np_arr = np.concatenate((np_arr, box), axis=0) + + # find the delaunay triangulation + log("calculating Delaunay triangulation") + delaunay = Delaunay(np_arr) + + # find voronoi polygon array + log("creating voronoi polygons from Delaunay triangulation") + # if edge_points: + # _, edge_arr = fc_to_numpy_array(edge_points) + # vor_arr = voronoi(delaunay) + # else: + # vor_arr = voronoi(delaunay) + vor_arr = voronoi(delaunay) + features = [] + for feature in vor_arr: + array = arcpy.Array([arcpy.Point(*coords) for coords in feature]) + polyline = arcpy.Polyline(array, spatial_reference=spatial_ref) + features.append(polyline) + + # create output fc from polygons + arcpy.management.CopyFeatures(features, output_file) + return + + + #### + + arcpy.da.NumPyArrayToFeatureClass(circum_centers, output_file, ["X","Y"], spatial_ref) + return + + # TODO: handle multiple polygons / points # create scratch layers - scratch_polygon = arcpy.CreateScratchName("polygon", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) - scratch_vertices = arcpy.CreateScratchName("vertices", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) - scratch_thiessen = arcpy.CreateScratchName("thiessen", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) - scratch_line = arcpy.CreateScratchName("line", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) - scratch_dissolve = arcpy.CreateScratchName("dissolve", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) - scratch_dangling = arcpy.CreateScratchName("dangling", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_polygon = arcpy.CreateScratchName("polygon", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_vertices = arcpy.CreateScratchName("vertices", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_thiessen = arcpy.CreateScratchName("thiessen", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_line = arcpy.CreateScratchName("line", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_dissolve = arcpy.CreateScratchName("dissolve", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + # scratch_dangling = arcpy.CreateScratchName("dangling", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) # export polygon to scratch - log("export") - arcpy.management.CopyFeatures(polygon, scratch_polygon) + # log("export") + # arcpy.management.CopyFeatures(polygon, scratch_polygon) + # densify log("densify") From 2a879d347f0259766b74f5426aeee1b2c9fa4a50 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 13 Jul 2026 11:14:08 -0400 Subject: [PATCH 17/42] ensure statistics will be correct --- src/TerrainAnalysis/VBET.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/TerrainAnalysis/VBET.py b/src/TerrainAnalysis/VBET.py index f2b04ff..5b0e160 100644 --- a/src/TerrainAnalysis/VBET.py +++ b/src/TerrainAnalysis/VBET.py @@ -198,6 +198,7 @@ def execute(self, parameters, _): log("creating scratch layers") scratch_buffer = arcpy.CreateScratchName("buffer", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) scratch_area = arcpy.CreateScratchName("area", data_type="FeatureClass", workspace=arcpy.env.scratchGDB) + max_scratch = arcpy.CreateScratchName("max", data_type="RasterDataset", workspace=arcpy.env.scratchGDB) # buffer streams, each stream segment a separate buffer segment log("creating buffer around stream lines") @@ -294,14 +295,14 @@ def execute(self, parameters, _): cellsize_type = "MinOf", ignore_nodata = True, ) - max_stats = arcpy.management.CalculateStatistics(max_raster) + max_raster.save(max_scratch) # threshold to full valley bottom and low-lying valley bottom # # full valley bottom = 0.65, low lying valley bottom = 0.85 log("thresholding output probability to find full valley bottom and low-lying valley bottom areas") - full_valley = arcpy.sa.Con(max_stats, 1, where_clause="VALUE >= 0.65") - low_lying = arcpy.sa.Con(max_stats, 1, where_clause="VALUE >= 0.85") + full_valley = arcpy.sa.Con(max_scratch, 1, where_clause="VALUE >= 0.65") + low_lying = arcpy.sa.Con(max_scratch, 1, where_clause="VALUE >= 0.85") # polygonize outputs log("converting outputs to polygons") From 1865c23d91af79df287f765fc108a001b61ece87 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 13 Jul 2026 11:15:18 -0400 Subject: [PATCH 18/42] allow for multiple workspaces to be submitted --- src/Utilities/RemoveUnused.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Utilities/RemoveUnused.py b/src/Utilities/RemoveUnused.py index 44cd645..3be5977 100644 --- a/src/Utilities/RemoveUnused.py +++ b/src/Utilities/RemoveUnused.py @@ -37,6 +37,7 @@ def getParameterInfo(self): displayName="Workspace", name="workspace", datatype="DEWorkspace", + multiValue="True", parameterType="Required", direction="Input") @@ -58,19 +59,23 @@ def updateParameters(self, parameters): # find unused if not parameters[0].hasBeenValidated: if parameters[0].value: - workspace = parameters[0].value + used = set() + maps = self.project.listMaps() + for m in maps: + for layer in m.listLayers(): + if layer.supports("DATASOURCE"): + used.add(layer.dataSource) options = set() - - # get all filepaths from workspace - for dirpath, dirnames, filenames in arcpy.da.Walk(workspace): - for filename in filenames: - fc = os.path.join(dirpath, filename) - options.add(fc) + workspaces = parameters[0].valueAsText.replace("'","").split(";") + for workspace in workspaces: + # get all filepaths from workspace + for dirpath, _, filenames in arcpy.da.Walk(workspace): + for filename in filenames: + fc = os.path.join(dirpath, filename) + options.add(fc) # remove all used filepaths - maps = self.project.listMaps() - for m in maps: - options = options - set(l.dataSource for l in m.listLayers() if l.supports("DATASOURCE")) + options = options - used parameters[1].filter.list = list(options) else: From 5073eef2bf6020b243ec5399ce81938c1a6cb9c4 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 13 Jul 2026 11:16:24 -0400 Subject: [PATCH 19/42] PIXEL_TYPE enum instead of dictionary --- src/helpers/rasters.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/helpers/rasters.py b/src/helpers/rasters.py index 17783a6..e9d2efc 100644 --- a/src/helpers/rasters.py +++ b/src/helpers/rasters.py @@ -7,26 +7,26 @@ # ----------------------------------------------------------------------------------- import arcpy +from enum import Enum from .units import convert_area, convert_length, LINEAR_TO_AREAL, SPATIAL_TO_LINEAR -PIXEL_TYPES = { - "U1": "1_BIT", - "U2": "2_BIT", - "U4": "4_BIT", - "S8": "8_BIT_SIGNED", - "U8": "8_BIT_UNSIGNED", - "S16": "16_BIT_UNSIGNED", - "U16": "16_BIT_SIGNED", - "S32": "32_BIT_UNSIGNED", - "U32": "32_BIT_SIGNED", - "F32": "32_BIT_FLOAT", - "F64": "64_BIT" -} - -def pixel_type(raster) -> str: +class PIXEL_TYPE(Enum): + U1="1_BIT", + U2 = "2_BIT" + U4 = "4_BIT" + S8 = "8_BIT_SIGNED" + U8 = "8_BIT_UNSIGNED" + S16 = "16_BIT_UNSIGNED" + U16 = "16_BIT_SIGNED" + S32 = "32_BIT_UNSIGNED" + U32 = "32_BIT_SIGNED" + F32 = "32_BIT_FLOAT" + F64 = "64_BIT" + +def pixel_type(raster) -> PIXEL_TYPE: """Return the the string representation of the raster pixel type.""" - return PIXEL_TYPES[raster.pixelType] + return PIXEL_TYPE[raster.pixelType] def cell_area(raster, area_unit=None) -> str: """Return the cell size of a RASTER as a GPArealUnit. User can specify unit AREA_UNIT From f1aac11afffb880111ef704baa3d38217ce8af96 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 14 Jul 2026 11:56:20 -0400 Subject: [PATCH 20/42] update metadata --- SWCD Tools.pyt.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SWCD Tools.pyt.xml b/SWCD Tools.pyt.xml index b30ad2b..86bb31a 100644 --- a/SWCD Tools.pyt.xml +++ b/SWCD Tools.pyt.xml @@ -1,2 +1,2 @@ -20251119093401001.0TRUE202606251810281500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 +20251119093401001.0TRUE202607081831381500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 From 05c5a2b2bbd258b5299c2aec46fab86ea3d826ca Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 14 Jul 2026 12:26:45 -0400 Subject: [PATCH 21/42] use convert_area --- src/BufferTools/BufferPotential.py | 5 ++--- src/TerrainAnalysis/VBET.py | 7 ++----- src/TileDrainage/DecisionTree.py | 7 ++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/BufferTools/BufferPotential.py b/src/BufferTools/BufferPotential.py index f3ad0ba..703b2b5 100644 --- a/src/BufferTools/BufferPotential.py +++ b/src/BufferTools/BufferPotential.py @@ -7,7 +7,7 @@ # -------------------------------------------------------------------------------- import arcpy -from ..helpers import license, empty_workspace, reload_module, log, raster_and_layer +from ..helpers import license, empty_workspace, reload_module, log, raster_and_layer, convert_area from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -163,8 +163,7 @@ def execute(self, parameters, messages): log("reading in parameters") stream = parameters[0].value min_width = parameters[1].valueAsText - min_acres, min_acres_unit = parameters[2].valueAsText.split(" ") - min_acres = float(min_acres) * arcpy.ArealUnitConversionFactor(min_acres_unit, "AcresUS") + min_acres, _ = convert_area(parameters[2].valueAsText, "AcresUS").split(" ") extent = parameters[3].value output_file = parameters[4].valueAsText land_use_raster, _ = raster_and_layer(parameters[5].value) diff --git a/src/TerrainAnalysis/VBET.py b/src/TerrainAnalysis/VBET.py index 5b0e160..7517a65 100644 --- a/src/TerrainAnalysis/VBET.py +++ b/src/TerrainAnalysis/VBET.py @@ -9,7 +9,7 @@ import arcpy from ..TerrainAnalysis import relative_elevation_model -from ..helpers import license, reload_module, log, empty_workspace, get_z_unit, is_empty, raster_and_layer, Z_UNITS, AREAL_UNITS, AREAL_UNITS_MAP +from ..helpers import license, reload_module, log, empty_workspace, get_z_unit, is_empty, raster_and_layer, convert_area, Z_UNITS, AREAL_UNITS, AREAL_UNITS_MAP from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -184,7 +184,7 @@ def execute(self, parameters, _): watershed_size_field = parameters[5].valueAsText watershed_area_unit = AREAL_UNITS_MAP[parameters[6].valueAsText] buffer_radius = parameters[7].valueAsText - min_watershed_size, min_watershed_unit = parameters[8].valueAsText.split(" ") if parameters[8].value is not None else (None, None) + min_watershed_size, _ = convert_area(parameters[8].valueAsText, watershed_area_unit).split(" ") if parameters[8].value else (None, None) full_valley_file = parameters[9].valueAsText low_lying_file = parameters[10].valueAsText remove = parameters[11].value @@ -231,9 +231,6 @@ def execute(self, parameters, _): log("creating watershed size thresholds") threshold_low = 25 * arcpy.ArealUnitConversionFactor("SquareKilometers", watershed_area_unit) threshold_high = 250 * arcpy.ArealUnitConversionFactor("SquareKilometers", watershed_area_unit) - if min_watershed_size is not None: - log("limiting watershed size") - min_watershed_size = float(min_watershed_size) * arcpy.ArealUnitConversionFactor(min_watershed_unit, watershed_area_unit) # set watershed size boundaries queries = [ diff --git a/src/TileDrainage/DecisionTree.py b/src/TileDrainage/DecisionTree.py index fda62aa..72103f5 100644 --- a/src/TileDrainage/DecisionTree.py +++ b/src/TileDrainage/DecisionTree.py @@ -8,7 +8,7 @@ import arcpy -from ..helpers import license, get_oid, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS +from ..helpers import license, get_oid, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS, convert_area from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -185,10 +185,7 @@ def execute(self, parameters, messages): land_use_raster = parameters[6].value land_use_field = parameters[7].value land_use_values = parameters[8].valueAsText.replace("'","").split(";") - num_acres, num_acres_unit = "", "" - if parameters[9].value: - num_acres, num_acres_unit = parameters[9].valueAsText.split(" ") - num_acres = float(num_acres) * arcpy.ArealUnitConversionFactor(num_acres_unit, "AcresUS") + num_acres = float(convert_area(parameters[9].valueAsText, "AcresUS").split(" ")[0]) if parameters[9].value else None # set analysis extent if extent: From b9680c89bb197d4774de83ccb63f50370f142b87 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 14 Jul 2026 12:28:05 -0400 Subject: [PATCH 22/42] describe types --- src/TileDrainage/DecisionTree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TileDrainage/DecisionTree.py b/src/TileDrainage/DecisionTree.py index 72103f5..35674f2 100644 --- a/src/TileDrainage/DecisionTree.py +++ b/src/TileDrainage/DecisionTree.py @@ -202,8 +202,8 @@ def execute(self, parameters, messages): # select viable land uses from land use raster log("extracting desired land uses") - scratch_land_use = None - existing_values = [] + scratch_land_use: arcpy.Raster + existing_values: list[str] with arcpy.da.SearchCursor(land_use_raster, land_use_field) as cursor: existing_values = sorted({row[0] for row in cursor}) land_use_values = [ i for i in land_use_values if i in existing_values ] From a1bf51f503b97332410a1bae808f6b5811b3e33e Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 14 Jul 2026 12:37:45 -0400 Subject: [PATCH 23/42] use convert_length --- src/Hydrology/WatershedDelineation.py | 5 ++--- src/TerrainAnalysis/LandscapePosition.py | 10 ++++------ src/TerrainModification/BermAnalysis.py | 11 +++-------- src/TerrainModification/BurnCulverts.py | 7 +++---- src/Utilities/LocalMinimums.py | 5 ++--- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Hydrology/WatershedDelineation.py b/src/Hydrology/WatershedDelineation.py index 6f09237..c1466d7 100644 --- a/src/Hydrology/WatershedDelineation.py +++ b/src/Hydrology/WatershedDelineation.py @@ -8,7 +8,7 @@ import arcpy -from ..helpers import license, get_oid, Z_UNITS, get_z_unit, reload_module, log, raster_and_layer +from ..helpers import license, get_oid, Z_UNITS, get_z_unit, reload_module, log, raster_and_layer, convert_length from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -116,8 +116,7 @@ def execute(self, parameters, messages): z_unit = parameters[1].value extent = parameters[2].value pour_points = parameters[3].value - snap_adjustment, snap_adjustment_unit = parameters[4].valueAsText.split(" ") - snap_adjustment = float(snap_adjustment) * arcpy.LinearUnitConversionFactor(snap_adjustment_unit, z_unit) + snap_adjustment = float(convert_length(parameters[4].valueAsText, z_unit).split(" ")[0]) output_file = parameters[5].valueAsText # set analysis extent diff --git a/src/TerrainAnalysis/LandscapePosition.py b/src/TerrainAnalysis/LandscapePosition.py index f8b171d..91dbeb7 100644 --- a/src/TerrainAnalysis/LandscapePosition.py +++ b/src/TerrainAnalysis/LandscapePosition.py @@ -10,7 +10,7 @@ import arcpy from ..TerrainAnalysis import topographic_position_index -from ..helpers import license, reload_module, log, get_z_unit, raster_and_layer, Z_UNITS +from ..helpers import license, reload_module, log, get_z_unit, raster_and_layer, convert_length, Z_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -116,14 +116,15 @@ def execute(self, parameters, messages): # Setup log("setting up project") project, active_map = setup() + map_unit = active_map.mapUnits # read in parameters log("reading in parameters") dem, _ = raster_and_layer(parameters[0].value) z_unit = parameters[1].value extent = parameters[2].value - radius_small, radius_small_unit = parameters[3].valueAsText.split(" ") - radius_large, radius_large_unit = parameters[4].valueAsText.split(" ") + radius_small = float(convert_length(parameters[3].valueAsText, map_unit).split(" ")[0]) + radius_large = float(convert_length(parameters[4].valueAsText, map_unit).split(" ")[0]) output_file = parameters[5].valueAsText # set analysis extent @@ -131,9 +132,6 @@ def execute(self, parameters, messages): arcpy.env.extent = extent # create neighborhoods - map_unit = active_map.mapUnits - radius_small = float(radius_small) * arcpy.LinearUnitConversionFactor(radius_small_unit, map_unit) - radius_large = float(radius_large) * arcpy.LinearUnitConversionFactor(radius_large_unit, map_unit) neighborhood_small = arcpy.sa.NbrCircle(radius_small, "MAP") neighborhood_large = arcpy.sa.NbrCircle(radius_large, "MAP") diff --git a/src/TerrainModification/BermAnalysis.py b/src/TerrainModification/BermAnalysis.py index c0a3b80..f4e3393 100644 --- a/src/TerrainModification/BermAnalysis.py +++ b/src/TerrainModification/BermAnalysis.py @@ -11,7 +11,7 @@ import arcpy -from ..helpers import license, get_oid, pixel_type, get_z_unit, Z_UNITS, empty_workspace, sanitize, set_required_parameter, reload_module, log, warn, is_empty, raster_and_layer +from ..helpers import license, get_oid, pixel_type, get_z_unit, Z_UNITS, empty_workspace, sanitize, set_required_parameter, reload_module, log, warn, is_empty, raster_and_layer, convert_length from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -194,12 +194,8 @@ def execute(self, parameters, messages): berm_z_factor = arcpy.LinearUnitConversionFactor(z_unit, berm_unit) # optionally specify contour interval contour_bool = parameters[8].value - contour_interval, contour_unit, contour_output = "", "", "" - if contour_bool: - contour_interval, contour_unit = parameters[9].valueAsText.split(" ") - contour_interval = float(contour_interval) - contour_z_factor = arcpy.LinearUnitConversionFactor(z_unit, contour_unit) - contour_output = parameters[10].valueAsText + contour_interval = float(convert_length(parameters[9].valueAsText, z_unit).split(" ")[0]) if contour_bool else None + contour_output = parameters[10].valueAsText # set analysis extent arcpy.env.extent = extent @@ -360,7 +356,6 @@ def execute(self, parameters, messages): out_polyline_features=scratch_contour, contour_interval=contour_interval, base_contour=0, - z_factor=contour_z_factor, ) # append contour outputs contour_output diff --git a/src/TerrainModification/BurnCulverts.py b/src/TerrainModification/BurnCulverts.py index 2587f33..377d670 100644 --- a/src/TerrainModification/BurnCulverts.py +++ b/src/TerrainModification/BurnCulverts.py @@ -9,7 +9,7 @@ import os import arcpy -from ..helpers import license, get_oid, pixel_type, empty_workspace, reload_module, log, raster_and_layer +from ..helpers import license, get_oid, pixel_type, empty_workspace, reload_module, log, raster_and_layer, convert_length from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -93,6 +93,7 @@ def execute(self, parameters, messages): # Setup log("setting up project") project, active_map = setup() + linear_unit = active_map.spatialReference.linearUnitName # read in parameters dem, dem_layer = raster_and_layer(parameters[0].value) @@ -104,9 +105,7 @@ def execute(self, parameters, messages): culverts = parameters[4].value desc = arcpy.Describe(culverts) spatial_reference = desc.spatialReference - distance, distance_unit = parameters[5].valueAsText.split(" ") - linear_unit = active_map.spatialReference.linearUnitName - distance = float(distance) * arcpy.LinearUnitConversionFactor(distance_unit, linear_unit) + distance = float(convert_length(parameters[5].valueAsText, linear_unit).split(" ")[0]) # set analysis extent if extent: diff --git a/src/Utilities/LocalMinimums.py b/src/Utilities/LocalMinimums.py index 8d149b2..7736dbb 100644 --- a/src/Utilities/LocalMinimums.py +++ b/src/Utilities/LocalMinimums.py @@ -13,7 +13,7 @@ import arcpy -from ..helpers import license, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS +from ..helpers import license, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, convert_length, Z_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -137,8 +137,7 @@ def execute(self, parameters, messages): z_linear_unit = parameters[2].value extent = parameters[3].value search_interval = parameters[4].valueAsText - threshold, threshold_unit = parameters[5].valueAsText.split(" ") - threshold = float(threshold) * arcpy.LinearUnitConversionFactor(threshold_unit, z_linear_unit) + threshold = float(convert_length(parameters[5].valueAsText, z_linear_unit).split(" ")[0]) output_file = parameters[6].valueAsText # create scratch layers From 9dbc72df63055875e1a04bcff2c92f5f09a49fc8 Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 14 Jul 2026 12:41:06 -0400 Subject: [PATCH 24/42] drive-by: no longer need to define output coordinate system, this is now done in setup_environment() --- src/Utilities/LocalMinimums.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Utilities/LocalMinimums.py b/src/Utilities/LocalMinimums.py index 7736dbb..9eb6ef2 100644 --- a/src/Utilities/LocalMinimums.py +++ b/src/Utilities/LocalMinimums.py @@ -127,9 +127,6 @@ def execute(self, parameters, messages): # Setup log("setting up project") project, active_map = setup() - spatial_reference_name = active_map.spatialReference.name - spatial_reference = arcpy.SpatialReference(spatial_reference_name) - arcpy.env.outputCoordinateSystem = spatial_reference log("reading in parameters") line = parameters[0].value @@ -237,8 +234,7 @@ def execute(self, parameters, messages): if len(local_minimums) > 0: log("copying points to feature class") arcpy.management.CopyFeatures(local_minimums, output_file) - #log("defining spatial reference of feature") - #arcpy.management.DefineProjection(output_file,spatial_reference) + log("adding minimums to map") active_map.addDataFromPath(output_file) else: From 4637b005064eddad41ade2fd26d7f0713ec7178e Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 13:25:44 -0400 Subject: [PATCH 25/42] create unit classes --- src/helpers/units.py | 264 ++++++++++++++++++++++++++++++++----------- 1 file changed, 198 insertions(+), 66 deletions(-) diff --git a/src/helpers/units.py b/src/helpers/units.py index b625bbc..2c9cf52 100644 --- a/src/helpers/units.py +++ b/src/helpers/units.py @@ -7,6 +7,8 @@ # ----------------------------------------------------------------------------------------- import arcpy +from typing import Self +from enum import StrEnum def get_z_unit(fc) -> str | None: """Get z unit from spatial reference.""" @@ -27,67 +29,59 @@ def get_linear_unit(fc) -> str | None: except Exception: return fc.spatialReference.linearUnitName -# mapping of spatial reference linear unit to GPLinearUnit -SPATIAL_TO_LINEAR = { - "Meter": "Meters", - "Foot_US": "Feet", - "Foot": "FeetInt" -} - -# z-units available to rasters for VCS -Z_UNITS = list(SPATIAL_TO_LINEAR.keys()) # inferred from https://developers.arcgis.com/rest/services-reference/enterprise/gp-data-types/#gplinearunit # but accuracy is unclear since they only give "esriFeet" and other placeholders # to test accuracy every GPLinearUnit was logged in a script # -# maps parameter display representation to arcpy GPLinearUnit -LINEAR_UNITS_MAP = { - "Unknown": "Unknown", - "International Inches": "InchesInt", - "US Survey Inches": "Inches", - "International Feet": "FeetInt", - "US Survey Feet": "Feet", - "International Yards": "YardsInt", - "US Survey Yards": "Yards", - "Statute Miles": "MilesInt", - "US Survey Miles": "Miles", - "Millimeters": "Millimeters", - "Centimeters": "Centimeters", - "Decimeters": "Decimeters", - "Meters": "Meters", - "Kilometers": "Kilometers", - "US Survey Nautical Miles": "NauticalMiles", - "International Nautical Miles": "NauticalMilesInt", - "Points": "Points", - "Decimal Degrees": "DecimalDegrees", -} -LINEAR_UNITS = list(LINEAR_UNITS_MAP.keys()) +# map arcpy GPLinearUnit to parameter display representation +LINEAR_UNITS = StrEnum("LINEAR_UNITS", { + "Unknown" : "Unknown", + "InchesInt" : "International Inches", + "Inches" : "US Survey Inches", + "FeetInt" : "International Feet", + "Feet" : "US Survey Feet", + "YardsInt" : "International Yards", + "Yards" : "US Survey Yards", + "MilesInt" : "Statute Miles", + "Miles" : "US Survey Miles", + "Millimeters" : "Millimeters", + "Centimeters" : "Centimeters", + "Decimeters" : "Decimeters", + "Meters" : "Meters", + "Kilometers" : "Kilometers", + "NauticalMiles" : "US Survey Nautical Miles", + "NauticalMilesInt" : "International Nautical Miles", + "Points" : "Points", + "DecimalDegrees" : "Decimal Degrees", +}) + # https://developers.arcgis.com/rest/services-reference/enterprise/gp-data-types/#gparealunit # -# maps parameter display representation to arcpy GPArealUnit -AREAL_UNITS_MAP = { - "Unknown": "Unknown", - "Square International Inches": "SquareInches", - "Square US Inches": "SquareInchesUS", - "Square International Feet": "SquareFeet", - "Square US Feet": "SquareFeetUS", - "Square International Yards": "SquareYards", - "Square US Yards": "SquareYardsUS", - "International Acres": "Acres", - "US Survey Acres": "AcresUS", - "Square Statute Miles": "SquareMiles", - "Square US Survey Miles": "SquareMilesUS", - "Square Millimeters": "SquareMillimeters", - "Square Centimeters": "SquareCentimeters", - "Square Decimeters": "SquareDecimeters", - "Square Meters": "SquareMeters", - "Square Kilometers": "SquareKilometers", - "Ares": "Ares", - "Hectares": "Hectares", -} -AREAL_UNITS = list(AREAL_UNITS_MAP.keys()) +# map arcpy GPArealUnit to parameter display representation +# AREAL_UNITS = { +AREAL_UNITS = StrEnum("AREAL_UNITS", { + "Unknown" : "Unknown", + "SquareInches" : "Square International Inches", + "SquareInchesUS" : "Square US Inches", + "SquareFeet" : "Square International Feet", + "SquareFeetUS" : "Square US Feet", + "SquareYards" : "Square International Yards", + "SquareYardsUS" : "Square US Yards", + "Acres" : "International Acres", + "AcresUS" : "US Survey Acres", + "SquareMiles" : "Square Statute Miles", + "SquareMilesUS" : "Square US Survey Miles", + "SquareMillimeters" : "Square Millimeters", + "SquareCentimeters" : "Square Centimeters", + "SquareDecimeters" : "Square Decimeters", + "SquareMeters" : "Square Meters", + "SquareKilometers" : "Square Kilometers", + "Ares" : "Ares", + "Hectares" : "Hectares", +}) + # mapping of GPLinearUnit to GPArealUnit (square units) # not all units have a mapping @@ -115,16 +109,154 @@ def get_linear_unit(fc) -> str | None: "DecimalDegrees": "Unknown", } -def convert_area(area: str, output_unit: str) -> str: - """Convert AREA to OUTPUT_UNIT factoring in area size.""" - size, from_unit = area.split(" ") - output_size = float(size) * arcpy.ArealUnitConversionFactor(from_unit, output_unit) - output_area = "{} {}".format(output_size, output_unit) - return output_area - -def convert_length(length: str, output_unit: str) -> str: - """Convert LENGTH to OUTPUT_UNIT factoring in length size.""" - size, from_unit = length.split(" ") - output_size = float(size) * arcpy.LinearUnitConversionFactor(from_unit, output_unit) - output_length = "{} {}".format(output_size, output_unit) - return output_length +# mapping of spatial reference linear unit to GPLinearUnit +SPATIAL_TO_LINEAR = { + "Meter": "Meters", + "Foot_US": "Feet", + "Foot": "FeetInt" +} + +# z-units available to rasters for VCS +Z_UNITS = list(SPATIAL_TO_LINEAR.keys()) + +class BaseUnit: + def __init__(self: Self, amount: float | int, unit: LINEAR_UNITS | AREAL_UNITS): + self.amount = amount + self.base_unit = unit + def __str__(self) -> str: + return "{} {}".format(self.amount, self.base_unit.value) + def __repr__(self) -> str: + return "{} {}".format(self.amount, self.base_unit.value) + def __mul__(self: Self, scalar: int | float) -> Self: + # Multiply + self.amount *= scalar + return self + def __truediv__(self: Self, divisor: int | float) -> Self: + # Divide + self.amount *= divisor + return self + def __mod__(self: Self, divisor: int | float) -> Self: + # Modulo + self.amount %= divisor + return self + def __floordiv__(self: Self, divisor: int | float) -> Self: + # Integer division + self.amount = self.amount // divisor + return self + + +class LinearUnit(BaseUnit): + def __init__(self: Self, value: str): + length, unit = value.split(" ") + # unit = unit_str if unit_str in LINEAR_UNITS else LINEAR_UNITS[unit_str] + super().__init__(amount=float(length), unit=LINEAR_UNITS[unit]) + @property + def length(self) -> int | float: + return self.amount + @length.setter + def length(self: Self, value: int | float) -> None: + self.amount = value + return + @property + def unit(self) -> LINEAR_UNITS: + return LINEAR_UNITS[self.base_unit.name] + @unit.setter + def unit(self: Self, unit: LINEAR_UNITS) -> None: + self.base_unit = unit + return + def to_unit(self: Self, output_unit: LINEAR_UNITS) -> Self: + """Convert LinearUnit to output_unit factoring in length size.""" + self.length = self.length * arcpy.LinearUnitConversionFactor(self.unit.name, output_unit.name) + return self + def __eq__(self: Self, other) -> bool: + # Equals + if not isinstance(other, LinearUnit): + return False + else: + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + return self.length == other_length + def __ne__(self: Self, other) -> bool: + # Not equals + return not self.__eq__(other) + def __lt__(self: Self, other: Self) -> bool: + # Less than + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + return self.length < other_length + def __gt__(self: Self, other: Self) -> bool: + # Greater than + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + return self.length > other_length + def __le__(self: Self, other: Self) -> bool: + # Less or equal + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + return self.length <= other_length + def __ge__(self: Self, other: Self) -> bool: + # Greater or equal + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + return self.length >= other_length + def __add__(self: Self, other: Self) -> Self: + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + self.length += other_length + return self + def __sub__(self: Self, other: Self) -> Self: + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + self.length -= other_length + return self + + +class ArealUnit(BaseUnit): + def __init__(self: Self, value: str): + amount, unit = value.split(" ") + super().__init__(amount=float(amount), unit=AREAL_UNITS[unit]) + @property + def area(self) -> int | float: + return self.amount + @area.setter + def area(self: Self, value: int | float) -> None: + self.amount = value + return + @property + def unit(self) -> AREAL_UNITS: + return AREAL_UNITS[self.base_unit.name] + @unit.setter + def unit(self: Self, unit: AREAL_UNITS) -> None: + self.base_unit = unit + return + def to_unit(self: Self, output_unit: AREAL_UNITS) -> Self: + """Convert LinearUnit to output_unit factoring in area size.""" + self.area = self.area * arcpy.ArealUnitConversionFactor(self.unit.name, output_unit.name) + return self + def __eq__(self: Self, other) -> bool: + # Equals + if not isinstance(other, ArealUnit): + return False + else: + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + return self.area == other_area + def __ne__(self: Self, other) -> bool: + # Not equals + return not self.__eq__(other) + def __lt__(self: Self, other: Self) -> bool: + # Less than + other_area = other.length * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + return self.length < other_area + def __gt__(self: Self, other: Self) -> bool: + # Greater than + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + return self.area > other_area + def __le__(self: Self, other: Self) -> bool: + # Less or equal + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + return self.area <= other_area + def __ge__(self: Self, other: Self) -> bool: + # Greater or equal + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + return self.area >= other_area + def __add__(self: Self, other: Self) -> Self: + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + self.area += other_area + return self + def __sub__(self: Self, other: Self) -> Self: + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + self.area -= other_area + return self From 8d044eab0e49d53266f4fd0387c7ed6967a403a2 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 13:26:34 -0400 Subject: [PATCH 26/42] use unit classes --- src/helpers/rasters.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/helpers/rasters.py b/src/helpers/rasters.py index e9d2efc..6315717 100644 --- a/src/helpers/rasters.py +++ b/src/helpers/rasters.py @@ -9,7 +9,7 @@ import arcpy from enum import Enum -from .units import convert_area, convert_length, LINEAR_TO_AREAL, SPATIAL_TO_LINEAR +from .units import LINEAR_TO_AREAL, SPATIAL_TO_LINEAR, LinearUnit, ArealUnit, LINEAR_UNITS, AREAL_UNITS class PIXEL_TYPE(Enum): U1="1_BIT", @@ -28,7 +28,7 @@ def pixel_type(raster) -> PIXEL_TYPE: """Return the the string representation of the raster pixel type.""" return PIXEL_TYPE[raster.pixelType] -def cell_area(raster, area_unit=None) -> str: +def cell_area(raster, to_unit: AREAL_UNITS | None = None) -> ArealUnit: """Return the cell size of a RASTER as a GPArealUnit. User can specify unit AREA_UNIT for output GPArealUnit to be in.""" # Note: throws an error if not a raster, this is desirable and shouldn't be used on @@ -43,14 +43,14 @@ def cell_area(raster, area_unit=None) -> str: area=cellsize_x * cellsize_y # output area - area = "{} {}".format(area, square_unit) + area = ArealUnit("{} {}".format(area, square_unit)) - if area_unit is not None: - area = convert_area(area, area_unit) + if to_unit is not None: + area = area.to_unit(to_unit) return area -def cell_length(raster, length_unit=None) -> str: +def cell_length(raster, to_unit=None) -> LinearUnit: """Return the average cell length of a RASTER as a GPLinearUnit. User can specify unit LINEAR_UNIT for output GPLinearUnit to be in.""" # Note: throws an error if not a raster, this is desirable and shouldn't be used on @@ -64,34 +64,39 @@ def cell_length(raster, length_unit=None) -> str: average_length = (cellsize_y + cellsize_x) / 2 # output length - length = "{} {}".format(average_length, linear_unit) + length = LinearUnit("{} {}".format(average_length, linear_unit)) - if length_unit is not None: - length = convert_length(length, length_unit) + if to_unit is not None: + length = length.to_unit(to_unit) return length -def cells_per_area(raster, area: str) -> int: +def cells_per_area(raster, area: ArealUnit) -> int: """Convert GPArealUnit AREA to the number of cells in the RASTER it is equivalent to.""" - cell_size, cell_unit = cell_area(raster).split(" ") + raster_cell_area = cell_area(raster) + cell_size = raster_cell_area.area + cell_unit = raster_cell_area.unit # convert area to raster cell unit - area_size_in_cell_units = convert_area(area, cell_unit).split(" ")[0] + area_size_in_cell_units = area.to_unit(cell_unit).area # find number of cells - num_cells = float(area_size_in_cell_units) / float(cell_size) + num_cells = area_size_in_cell_units / cell_size return int(num_cells) -def cells_per_length(raster, length: str) -> int: +def cells_per_length(raster, length: LinearUnit) -> int: """Convert GPLinearUnit LENGTH to the number of cells in the RASTER it is equivalent to.""" - cell_size, cell_unit = cell_length(raster).split(" ") + raster_cell_length = cell_length(raster) + cell_size = raster_cell_length.length + cell_unit = raster_cell_length.unit + # convert length to raster cell unit - area_size_in_cell_units = convert_length(length, cell_unit).split(" ")[0] + area_size_in_cell_units = length.to_unit(cell_unit).length # find number of cells - num_cells = float(area_size_in_cell_units) / float(cell_size) + num_cells = area_size_in_cell_units / cell_size return int(num_cells) @@ -102,7 +107,7 @@ def min_cell_path(parameters) -> str: for param in parameters: try: # get cell size of param in US Acres - size_acres = float(cell_area(param.value, "AcresUS").split(" ")[0]) + size_acres = cell_area(param.value, AREAL_UNITS.AcresUS).area # compare sizes if min_cell_size is None or size_acres < min_cell_size: From 3a30092b84e4ca2baa659d66365024716d684adc Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 13:26:54 -0400 Subject: [PATCH 27/42] update __init__ with unit classes --- src/helpers/__init__.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py index c5f0746..3e937b9 100644 --- a/src/helpers/__init__.py +++ b/src/helpers/__init__.py @@ -35,14 +35,12 @@ get_z_unit, get_linear_unit, Z_UNITS, + LINEAR_TO_AREAL, + SPATIAL_TO_LINEAR, LINEAR_UNITS, AREAL_UNITS, - LINEAR_UNITS_MAP, - AREAL_UNITS_MAP, - SPATIAL_TO_LINEAR, - LINEAR_TO_AREAL, - convert_area, - convert_length, + LinearUnit, + ArealUnit, ) __all__ = [ @@ -69,14 +67,12 @@ "get_z_unit", "get_linear_unit", "Z_UNITS", + "LINEAR_TO_AREAL", + "SPATIAL_TO_LINEAR", "LINEAR_UNITS", "AREAL_UNITS", - "LINEAR_UNITS_MAP", - "AREAL_UNITS_MAP", - "SPATIAL_TO_LINEAR", - "LINEAR_TO_AREAL", - "convert_area", - "convert_length", + "LinearUnit", + "ArealUnit", "fc_to_numpy_array", "bbox", "delaunay_fc", From ccd73d5b680dfbbf9cb034b1e5f87c5986b8a75d Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:23:10 -0400 Subject: [PATCH 28/42] Create separate map of enum to display text and methods. Use enum for everything --- src/helpers/__init__.py | 4 ++ src/helpers/units.py | 141 +++++++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py index 3e937b9..0645b04 100644 --- a/src/helpers/__init__.py +++ b/src/helpers/__init__.py @@ -37,6 +37,8 @@ Z_UNITS, LINEAR_TO_AREAL, SPATIAL_TO_LINEAR, + LINEAR_UNITS_MAP, + AREAL_UNITS_MAP, LINEAR_UNITS, AREAL_UNITS, LinearUnit, @@ -67,6 +69,8 @@ "get_z_unit", "get_linear_unit", "Z_UNITS", + "LINEAR_UNITS_MAP", + "AREAL_UNITS_MAP", "LINEAR_TO_AREAL", "SPATIAL_TO_LINEAR", "LINEAR_UNITS", diff --git a/src/helpers/units.py b/src/helpers/units.py index 2c9cf52..ce65cfd 100644 --- a/src/helpers/units.py +++ b/src/helpers/units.py @@ -35,51 +35,56 @@ def get_linear_unit(fc) -> str | None: # to test accuracy every GPLinearUnit was logged in a script # # map arcpy GPLinearUnit to parameter display representation -LINEAR_UNITS = StrEnum("LINEAR_UNITS", { +LINEAR_UNITS_MAP = { "Unknown" : "Unknown", - "InchesInt" : "International Inches", - "Inches" : "US Survey Inches", - "FeetInt" : "International Feet", - "Feet" : "US Survey Feet", - "YardsInt" : "International Yards", - "Yards" : "US Survey Yards", - "MilesInt" : "Statute Miles", - "Miles" : "US Survey Miles", + "International Inches" : "InchesInt", + "US Survey Inches" : "Inches", + "International Feet" : "FeetInt", + "US Survey Feet" : "Feet", + "International Yards" : "YardsInt", + "US Survey Yards" : "Yards", + "Statute Miles" : "MilesInt", + "US Survey Miles" : "Miles", "Millimeters" : "Millimeters", "Centimeters" : "Centimeters", "Decimeters" : "Decimeters", "Meters" : "Meters", "Kilometers" : "Kilometers", - "NauticalMiles" : "US Survey Nautical Miles", - "NauticalMilesInt" : "International Nautical Miles", + "US Survey Nautical Miles" : "NauticalMiles", + "International Nautical Miles" : "NauticalMilesInt", "Points" : "Points", - "DecimalDegrees" : "Decimal Degrees", + "Decimal Degrees" : "DecimalDegrees", +} +LINEAR_UNITS = StrEnum("LINEAR_UNITS", { + i: i for i in LINEAR_UNITS_MAP.values() }) # https://developers.arcgis.com/rest/services-reference/enterprise/gp-data-types/#gparealunit # # map arcpy GPArealUnit to parameter display representation -# AREAL_UNITS = { -AREAL_UNITS = StrEnum("AREAL_UNITS", { +AREAL_UNITS_MAP = { "Unknown" : "Unknown", - "SquareInches" : "Square International Inches", - "SquareInchesUS" : "Square US Inches", - "SquareFeet" : "Square International Feet", - "SquareFeetUS" : "Square US Feet", - "SquareYards" : "Square International Yards", - "SquareYardsUS" : "Square US Yards", - "Acres" : "International Acres", - "AcresUS" : "US Survey Acres", - "SquareMiles" : "Square Statute Miles", - "SquareMilesUS" : "Square US Survey Miles", - "SquareMillimeters" : "Square Millimeters", - "SquareCentimeters" : "Square Centimeters", - "SquareDecimeters" : "Square Decimeters", - "SquareMeters" : "Square Meters", - "SquareKilometers" : "Square Kilometers", + "Square International Inches" : "SquareInches", + "Square US Inches" : "SquareInchesUS", + "Square International Feet" : "SquareFeet", + "Square US Feet" : "SquareFeetUS", + "Square International Yards" : "SquareYards", + "Square US Yards" : "SquareYardsUS", + "International Acres" : "Acres", + "US Survey Acres" : "AcresUS", + "Square Statute Miles" : "SquareMiles", + "Square US Survey Miles" : "SquareMilesUS", + "Square Millimeters" : "SquareMillimeters", + "Square Centimeters" : "SquareCentimeters", + "Square Decimeters" : "SquareDecimeters", + "Square Meters" : "SquareMeters", + "Square Kilometers" : "SquareKilometers", "Ares" : "Ares", "Hectares" : "Hectares", +} +AREAL_UNITS = StrEnum("AREAL_UNITS", { + i: i for i in AREAL_UNITS_MAP.values() }) @@ -119,14 +124,15 @@ def get_linear_unit(fc) -> str | None: # z-units available to rasters for VCS Z_UNITS = list(SPATIAL_TO_LINEAR.keys()) + class BaseUnit: def __init__(self: Self, amount: float | int, unit: LINEAR_UNITS | AREAL_UNITS): self.amount = amount self.base_unit = unit def __str__(self) -> str: - return "{} {}".format(self.amount, self.base_unit.value) + return "{} {}".format(self.amount, self.base_unit) def __repr__(self) -> str: - return "{} {}".format(self.amount, self.base_unit.value) + return "{} {}".format(self.amount, self.base_unit) def __mul__(self: Self, scalar: int | float) -> Self: # Multiply self.amount *= scalar @@ -144,12 +150,13 @@ def __floordiv__(self: Self, divisor: int | float) -> Self: self.amount = self.amount // divisor return self - class LinearUnit(BaseUnit): - def __init__(self: Self, value: str): - length, unit = value.split(" ") - # unit = unit_str if unit_str in LINEAR_UNITS else LINEAR_UNITS[unit_str] - super().__init__(amount=float(length), unit=LINEAR_UNITS[unit]) + def __init__(self: Self, input: str): + length, unit_str, *rest = input.split(" ") + if rest is not None: + unit_str += " " + " ".join(rest) + unit_str = LINEAR_UNITS_MAP[unit_str] + super().__init__(amount=float(length), unit=LINEAR_UNITS[unit_str]) @property def length(self) -> int | float: return self.amount @@ -159,55 +166,65 @@ def length(self: Self, value: int | float) -> None: return @property def unit(self) -> LINEAR_UNITS: - return LINEAR_UNITS[self.base_unit.name] + return LINEAR_UNITS[self.base_unit] @unit.setter def unit(self: Self, unit: LINEAR_UNITS) -> None: self.base_unit = unit return def to_unit(self: Self, output_unit: LINEAR_UNITS) -> Self: """Convert LinearUnit to output_unit factoring in length size.""" - self.length = self.length * arcpy.LinearUnitConversionFactor(self.unit.name, output_unit.name) + self.length = self.length * arcpy.LinearUnitConversionFactor(self.unit, output_unit) + self.unit = output_unit return self + def full_unit(self: Self) -> str: + unit = self.unit + for key, value in LINEAR_UNITS_MAP.items(): + if value == unit: + unit = key + break + return unit def __eq__(self: Self, other) -> bool: # Equals if not isinstance(other, LinearUnit): return False else: - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) return self.length == other_length def __ne__(self: Self, other) -> bool: # Not equals return not self.__eq__(other) def __lt__(self: Self, other: Self) -> bool: # Less than - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) return self.length < other_length def __gt__(self: Self, other: Self) -> bool: # Greater than - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) return self.length > other_length def __le__(self: Self, other: Self) -> bool: # Less or equal - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) return self.length <= other_length def __ge__(self: Self, other: Self) -> bool: # Greater or equal - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) return self.length >= other_length def __add__(self: Self, other: Self) -> Self: - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) self.length += other_length return self def __sub__(self: Self, other: Self) -> Self: - other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit.name, self.unit.name) + other_length = other.length * arcpy.LinearUnitConversionFactor(other.unit, self.unit) self.length -= other_length return self - class ArealUnit(BaseUnit): - def __init__(self: Self, value: str): - amount, unit = value.split(" ") - super().__init__(amount=float(amount), unit=AREAL_UNITS[unit]) + def __init__(self: Self, input: str): + area, unit_str, *rest = input.split(" ") + if rest is not None: + unit_str += " " + " ".join(rest) + unit_str = AREAL_UNITS_MAP[unit_str] + super().__init__(amount=float(area), unit=AREAL_UNITS[unit_str]) @property def area(self) -> int | float: return self.amount @@ -217,46 +234,54 @@ def area(self: Self, value: int | float) -> None: return @property def unit(self) -> AREAL_UNITS: - return AREAL_UNITS[self.base_unit.name] + return AREAL_UNITS[self.base_unit] @unit.setter def unit(self: Self, unit: AREAL_UNITS) -> None: self.base_unit = unit return def to_unit(self: Self, output_unit: AREAL_UNITS) -> Self: """Convert LinearUnit to output_unit factoring in area size.""" - self.area = self.area * arcpy.ArealUnitConversionFactor(self.unit.name, output_unit.name) + self.area = self.area * arcpy.ArealUnitConversionFactor(self.unit, output_unit) + self.unit = output_unit return self + def full_unit(self: Self) -> str: + unit = self.unit + for key, value in AREAL_UNITS_MAP.items(): + if value == unit: + unit = key + break + return unit def __eq__(self: Self, other) -> bool: # Equals if not isinstance(other, ArealUnit): return False else: - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) return self.area == other_area def __ne__(self: Self, other) -> bool: # Not equals return not self.__eq__(other) def __lt__(self: Self, other: Self) -> bool: # Less than - other_area = other.length * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.length * arcpy.ArealUnitConversionFactor(other.unit, self.unit) return self.length < other_area def __gt__(self: Self, other: Self) -> bool: # Greater than - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) return self.area > other_area def __le__(self: Self, other: Self) -> bool: # Less or equal - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) return self.area <= other_area def __ge__(self: Self, other: Self) -> bool: # Greater or equal - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) return self.area >= other_area def __add__(self: Self, other: Self) -> Self: - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) self.area += other_area return self def __sub__(self: Self, other: Self) -> Self: - other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit.name, self.unit.name) + other_area = other.area * arcpy.ArealUnitConversionFactor(other.unit, self.unit) self.area -= other_area return self From fe4a8e6dde18dd223831d74b01d54cb362ff6168 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:24:13 -0400 Subject: [PATCH 29/42] update metadata --- SWCD Tools.pyt.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SWCD Tools.pyt.xml b/SWCD Tools.pyt.xml index 86bb31a..9328d09 100644 --- a/SWCD Tools.pyt.xml +++ b/SWCD Tools.pyt.xml @@ -1,2 +1,2 @@ -20251119093401001.0TRUE202607081831381500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 +20251119093401001.0TRUE202607161333531500000005000ItemDescriptionc:\program files\arcgis\pro\Resources\Help\gpSWCD ToolsA collection of ArcGIS tools related to conservation and natural resourcesSoil and Water Toolbox GIS ArcGISArcToolbox Toolbox20251221 From 31045a9d6261f4ee6ee4655129a9761b9864aadb Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:24:49 -0400 Subject: [PATCH 30/42] use class methods for units --- src/helpers/rasters.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/helpers/rasters.py b/src/helpers/rasters.py index 6315717..523cd6d 100644 --- a/src/helpers/rasters.py +++ b/src/helpers/rasters.py @@ -29,7 +29,7 @@ def pixel_type(raster) -> PIXEL_TYPE: return PIXEL_TYPE[raster.pixelType] def cell_area(raster, to_unit: AREAL_UNITS | None = None) -> ArealUnit: - """Return the cell size of a RASTER as a GPArealUnit. User can specify unit AREA_UNIT + """Return the cell size of a RASTER as a GPArealUnit. User can specify unit AREA_UNITS for output GPArealUnit to be in.""" # Note: throws an error if not a raster, this is desirable and shouldn't be used on # data types other than a raster @@ -50,9 +50,9 @@ def cell_area(raster, to_unit: AREAL_UNITS | None = None) -> ArealUnit: return area -def cell_length(raster, to_unit=None) -> LinearUnit: +def cell_length(raster, to_unit: LINEAR_UNITS | None = None) -> LinearUnit: """Return the average cell length of a RASTER as a GPLinearUnit. User can specify - unit LINEAR_UNIT for output GPLinearUnit to be in.""" + unit LINEAR_UNITS for output GPLinearUnit to be in.""" # Note: throws an error if not a raster, this is desirable and shouldn't be used on # data types other than a raster desc_raster = arcpy.Describe(raster) @@ -91,7 +91,6 @@ def cells_per_length(raster, length: LinearUnit) -> int: cell_size = raster_cell_length.length cell_unit = raster_cell_length.unit - # convert length to raster cell unit area_size_in_cell_units = length.to_unit(cell_unit).length @@ -102,17 +101,19 @@ def cells_per_length(raster, length: LinearUnit) -> int: def min_cell_path(parameters) -> str: """Return the parameter with the smallest cell size.""" - min_cell_size = None + min_cell_size: ArealUnit | None = None min_cell_path = "MINOF" for param in parameters: try: - # get cell size of param in US Acres - size_acres = cell_area(param.value, AREAL_UNITS.AcresUS).area - - # compare sizes - if min_cell_size is None or size_acres < min_cell_size: - min_cell_size = size_acres + # get cell size of param + size = cell_area(param.value) + if min_cell_size is None: + min_cell_size = size min_cell_path = param.valueAsText + else: + if size < min_cell_size: + min_cell_size = size + min_cell_path = param.valueAsText except Exception: pass From 67b026bcf5d10c0d1717b450549e07f12e221b95 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:37:02 -0400 Subject: [PATCH 31/42] fix: check for empty list properly --- src/helpers/units.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/units.py b/src/helpers/units.py index ce65cfd..41c2488 100644 --- a/src/helpers/units.py +++ b/src/helpers/units.py @@ -153,7 +153,7 @@ def __floordiv__(self: Self, divisor: int | float) -> Self: class LinearUnit(BaseUnit): def __init__(self: Self, input: str): length, unit_str, *rest = input.split(" ") - if rest is not None: + if rest: unit_str += " " + " ".join(rest) unit_str = LINEAR_UNITS_MAP[unit_str] super().__init__(amount=float(length), unit=LINEAR_UNITS[unit_str]) @@ -221,7 +221,7 @@ def __sub__(self: Self, other: Self) -> Self: class ArealUnit(BaseUnit): def __init__(self: Self, input: str): area, unit_str, *rest = input.split(" ") - if rest is not None: + if rest: unit_str += " " + " ".join(rest) unit_str = AREAL_UNITS_MAP[unit_str] super().__init__(amount=float(area), unit=AREAL_UNITS[unit_str]) From 8aa34a1af0e3a578ca6b38c29de257e43c3a82b5 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:38:50 -0400 Subject: [PATCH 32/42] use unit classes --- src/BufferTools/BufferPotential.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/BufferTools/BufferPotential.py b/src/BufferTools/BufferPotential.py index 703b2b5..317b1f2 100644 --- a/src/BufferTools/BufferPotential.py +++ b/src/BufferTools/BufferPotential.py @@ -7,7 +7,7 @@ # -------------------------------------------------------------------------------- import arcpy -from ..helpers import license, empty_workspace, reload_module, log, raster_and_layer, convert_area +from ..helpers import license, empty_workspace, reload_module, log, warn, raster_and_layer, ArealUnit, LinearUnit from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -162,8 +162,8 @@ def execute(self, parameters, messages): log("reading in parameters") stream = parameters[0].value - min_width = parameters[1].valueAsText - min_acres, _ = convert_area(parameters[2].valueAsText, "AcresUS").split(" ") + min_width = LinearUnit(parameters[1].valueAsText) + min_area = ArealUnit(parameters[2].valueAsText) extent = parameters[3].value output_file = parameters[4].valueAsText land_use_raster, _ = raster_and_layer(parameters[5].value) @@ -187,7 +187,7 @@ def execute(self, parameters, messages): # pairwise buffer stream log("creating buffer polygon around stream") - arcpy.analysis.PairwiseBuffer(stream, scratch_stream_buffer, min_width, "ALL", "", "GEODESIC", "") + arcpy.analysis.PairwiseBuffer(stream, scratch_stream_buffer, str(min_width), "ALL", "", "GEODESIC", "") # clip land uses to buffer log("extracting land use data inside buffer area") @@ -240,11 +240,11 @@ def execute(self, parameters, messages): # calculate acreage log("calculating acreage of planting areas") - arcpy.management.AddField(scratch_dissolve, "Acres", "FLOAT", 2, 2) - arcpy.management.CalculateGeometryAttributes(scratch_dissolve, geometry_property=[["Acres", "AREA_GEODESIC"]], area_unit="ACRES_US") + arcpy.management.AddField(scratch_dissolve, min_area.unit, "FLOAT", 2, 2) + arcpy.management.CalculateGeometryAttributes(scratch_dissolve, geometry_property=[[min_area.unit, "AREA_GEODESIC"]], area_unit=min_area.full_unit()) # drop acreage < threshold - sql_query = "Acres >= {}".format(min_acres) + sql_query = "{} >= {}".format(min_area.unit, min_area.area) arcpy.analysis.Select(scratch_dissolve, output_file, sql_query) # add output to map From c218a34c4fd3dbd39299f5fa250fd13ff161a497 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:38:59 -0400 Subject: [PATCH 33/42] warn on empty output --- src/BufferTools/BufferPotential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BufferTools/BufferPotential.py b/src/BufferTools/BufferPotential.py index 317b1f2..bdc4f87 100644 --- a/src/BufferTools/BufferPotential.py +++ b/src/BufferTools/BufferPotential.py @@ -210,7 +210,7 @@ def execute(self, parameters, messages): land_use_sql_query += " Or {} = '{}'".format(land_use_field, value) scratch_land_use = arcpy.sa.ExtractByAttributes(land_use_raster_clip, land_use_sql_query) else: - log("no valid land uses found in area, please try again with land uses found in analysis area") + warn("no valid land uses found in area, please try again with land uses found in analysis area") return # convert land usage output to polygon From 94413173a58ddcdb81d4bed293d1422fc467edcb Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:50:06 -0400 Subject: [PATCH 34/42] use LinearUnit and remove Z_UNIT parameter --- src/Hydrology/WatershedDelineation.py | 59 ++++++++------------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/src/Hydrology/WatershedDelineation.py b/src/Hydrology/WatershedDelineation.py index c1466d7..98990c2 100644 --- a/src/Hydrology/WatershedDelineation.py +++ b/src/Hydrology/WatershedDelineation.py @@ -8,7 +8,7 @@ import arcpy -from ..helpers import license, get_oid, Z_UNITS, get_z_unit, reload_module, log, raster_and_layer, convert_length +from ..helpers import license, get_oid, Z_UNITS, get_z_unit, reload_module, log, raster_and_layer, LINEAR_UNITS, LinearUnit from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -29,47 +29,39 @@ def getParameterInfo(self): direction="Input") param1 = arcpy.Parameter( - displayName="Z Unit", - name="z_unit", - datatype="GPString", - parameterType="Required", - direction="Input") - param1.filter.list = Z_UNITS - - param2 = arcpy.Parameter( displayName="Analysis Area", name="analysis_area", datatype="GPExtent", parameterType="Optional", direction="Input") - param2.controlCLSID = '{15F0D1C1-F783-49BC-8D16-619B8E92F668}' + param1.controlCLSID = '{15F0D1C1-F783-49BC-8D16-619B8E92F668}' - param3 = arcpy.Parameter( + param2 = arcpy.Parameter( displayName="Pour Point", name="pourpoint", datatype="GPFeatureLayer", parameterType="Required", direction="Input") - param3.filter.list = ["Point"] - param3.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows point creation + param2.filter.list = ["Point"] + param2.controlCLSID = '{60061247-BCA8-473E-A7AF-A2026DDE1C2D}' # allows point creation - param4 = arcpy.Parameter( + param3 = arcpy.Parameter( displayName="Snap Pour Point Max Adjustment Distance", name="snap_adjustment", datatype="GPLinearUnit", parameterType="Required", direction="Input") - param5 = arcpy.Parameter( + param4 = arcpy.Parameter( displayName="Output Features", name="out_features", datatype="DEFeatureClass", parameterType="Required", direction="Output") - param5.parameterDependencies = [param0.name] - param5.schema.clone = True + param4.parameterDependencies = [param0.name] + param4.schema.clone = True - params = [param0, param1, param2, param3, param4, param5] + params = [param0, param1, param2, param3, param4] return params def isLicensed(self): @@ -77,26 +69,9 @@ def isLicensed(self): return license(['Spatial']) def updateParameters(self, parameters): - # find z unit of raster based on vertical coordinate system - # - if there is none, let the user define it - # - if it exists, set the value and hide the parameter - # - if it doesn't exist show the parameter and set the value to None - if not parameters[0].hasBeenValidated: - if parameters[0].value: - z_unit = get_z_unit(parameters[0].value) - if z_unit: - parameters[1].enabled = False - parameters[1].value = z_unit - else: - parameters[1].enabled = True - parameters[1].value = None - else: - parameters[1].enabled = False - parameters[1].value = None - # Default snap pour point adjustment value - if parameters[4].value is None: - parameters[4].value = "10 Meters" + if parameters[3].value is None: + parameters[3].value = "10 Meters" return @@ -110,14 +85,14 @@ def execute(self, parameters, messages): # Setup log("setting up project") project, active_map = setup() + map_unit = LINEAR_UNITS[active_map.mapUnits] # read in parameters dem, _ = raster_and_layer(parameters[0].value) - z_unit = parameters[1].value - extent = parameters[2].value - pour_points = parameters[3].value - snap_adjustment = float(convert_length(parameters[4].valueAsText, z_unit).split(" ")[0]) - output_file = parameters[5].valueAsText + extent = parameters[1].value + pour_points = parameters[2].value + snap_adjustment = LinearUnit(parameters[3].valueAsText).to_unit(map_unit).length + output_file = parameters[4].valueAsText # set analysis extent if extent: From b357833d922e868e3e03e73bd83058cb38ba56d2 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 17:58:11 -0400 Subject: [PATCH 35/42] use units classes --- src/Hydrology/WatershedSize.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Hydrology/WatershedSize.py b/src/Hydrology/WatershedSize.py index c86dd23..1a962db 100644 --- a/src/Hydrology/WatershedSize.py +++ b/src/Hydrology/WatershedSize.py @@ -7,7 +7,7 @@ # -------------------------------------------------------------------------------- import arcpy -from ..helpers import license, reload_module, log, AREAL_UNITS, AREAL_UNITS_MAP, cell_area, raster_and_layer +from ..helpers import license, reload_module, log, AREAL_UNITS_MAP, AREAL_UNITS, cell_area, raster_and_layer from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -41,7 +41,7 @@ def getParameterInfo(self): datatype="GPString", parameterType="Required", direction="Input") - param2.filter.list = AREAL_UNITS + param2.filter.list = [i for i in AREAL_UNITS_MAP.keys()] param2.value = "US Survey Acres" param3 = arcpy.Parameter( @@ -78,8 +78,7 @@ def execute(self, parameters, messages): log("reading in parameters") dem, _ = raster_and_layer(parameters[0].value) extent = parameters[1].value - # read in areal unit and map it's pretty string to the arcpy representation - areal_unit = AREAL_UNITS_MAP[parameters[2].valueAsText] + areal_unit = AREAL_UNITS[AREAL_UNITS_MAP[parameters[2].valueAsText]] output_file = parameters[3].valueAsText # set analysis extent @@ -100,7 +99,7 @@ def execute(self, parameters, messages): # convert flow accumulation from number of cells to area units log("calculating watershed size") - cell_size = float(cell_area(dem, areal_unit).split(" ")[0]) + cell_size = cell_area(dem, areal_unit).area watershed_size = flow_accumulation * cell_size # save output to file From ab6eda3fa252efc962deaa209002554b887e4525 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 18:02:43 -0400 Subject: [PATCH 36/42] use units class --- src/TerrainAnalysis/LandscapePosition.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/TerrainAnalysis/LandscapePosition.py b/src/TerrainAnalysis/LandscapePosition.py index 91dbeb7..9d11a3a 100644 --- a/src/TerrainAnalysis/LandscapePosition.py +++ b/src/TerrainAnalysis/LandscapePosition.py @@ -10,7 +10,7 @@ import arcpy from ..TerrainAnalysis import topographic_position_index -from ..helpers import license, reload_module, log, get_z_unit, raster_and_layer, convert_length, Z_UNITS +from ..helpers import license, reload_module, log, get_z_unit, raster_and_layer, Z_UNITS, LinearUnit, LINEAR_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -116,15 +116,15 @@ def execute(self, parameters, messages): # Setup log("setting up project") project, active_map = setup() - map_unit = active_map.mapUnits + map_unit = LINEAR_UNITS[active_map.mapUnits] # read in parameters log("reading in parameters") dem, _ = raster_and_layer(parameters[0].value) z_unit = parameters[1].value extent = parameters[2].value - radius_small = float(convert_length(parameters[3].valueAsText, map_unit).split(" ")[0]) - radius_large = float(convert_length(parameters[4].valueAsText, map_unit).split(" ")[0]) + radius_small = LinearUnit(parameters[3].valueAsText).to_unit(map_unit).length + radius_large = LinearUnit(parameters[4].valueAsText).to_unit(map_unit).length output_file = parameters[5].valueAsText # set analysis extent From dda0fc040e45e845b606acc1a383f52ba3c22161 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 18:16:10 -0400 Subject: [PATCH 37/42] use units classes --- src/TerrainAnalysis/VBET.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/TerrainAnalysis/VBET.py b/src/TerrainAnalysis/VBET.py index 7517a65..289758f 100644 --- a/src/TerrainAnalysis/VBET.py +++ b/src/TerrainAnalysis/VBET.py @@ -9,7 +9,7 @@ import arcpy from ..TerrainAnalysis import relative_elevation_model -from ..helpers import license, reload_module, log, empty_workspace, get_z_unit, is_empty, raster_and_layer, convert_area, Z_UNITS, AREAL_UNITS, AREAL_UNITS_MAP +from ..helpers import license, reload_module, log, empty_workspace, get_z_unit, is_empty, raster_and_layer, Z_UNITS, AREAL_UNITS_MAP, AREAL_UNITS, ArealUnit, LinearUnit from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate @@ -75,7 +75,7 @@ def getParameterInfo(self): datatype="GPString", parameterType="Required", direction="Input") - param6.filter.list = AREAL_UNITS + param6.filter.list = [i for i in AREAL_UNITS_MAP.keys()] param7 = arcpy.Parameter( displayName="Buffer Radius", @@ -182,13 +182,18 @@ def execute(self, parameters, _): rem, _ = raster_and_layer(parameters[3].value) if parameters[3].value is not None else (None, None) streams = parameters[4].value watershed_size_field = parameters[5].valueAsText - watershed_area_unit = AREAL_UNITS_MAP[parameters[6].valueAsText] - buffer_radius = parameters[7].valueAsText - min_watershed_size, _ = convert_area(parameters[8].valueAsText, watershed_area_unit).split(" ") if parameters[8].value else (None, None) + watershed_area_unit = AREAL_UNITS[AREAL_UNITS_MAP[parameters[6].valueAsText]] + buffer_radius = LinearUnit(parameters[7].valueAsText) + sampling_interval = LinearUnit("35 Feet") + min_watershed_size = ArealUnit(parameters[8].valueAsText).to_unit(watershed_area_unit).area if parameters[8].value is not None else None full_valley_file = parameters[9].valueAsText low_lying_file = parameters[10].valueAsText remove = parameters[11].value + log("creating watershed size thresholds") + threshold_low = ArealUnit("25 SquareKilometers").to_unit(watershed_area_unit).area + threshold_high = ArealUnit("250 SquareKilometers").to_unit(watershed_area_unit).area + # set analysis extent if extent: log("setting analysis extent") @@ -205,7 +210,7 @@ def execute(self, parameters, _): arcpy.analysis.PairwiseBuffer( in_features=streams, out_feature_class=scratch_buffer, - buffer_distance_or_field=buffer_radius, + buffer_distance_or_field=str(buffer_radius), dissolve_option="NONE", dissolve_field=None, method="GEODESIC", @@ -225,12 +230,15 @@ def execute(self, parameters, _): # optionally create REM if rem is None: log("calculating relative elevation model") - rem = relative_elevation_model(active_map, dem, extent, streams, buffer_radius, "35 Feet", resolve=True) - - # get threshold watershed sizes in km^2 - log("creating watershed size thresholds") - threshold_low = 25 * arcpy.ArealUnitConversionFactor("SquareKilometers", watershed_area_unit) - threshold_high = 250 * arcpy.ArealUnitConversionFactor("SquareKilometers", watershed_area_unit) + rem = relative_elevation_model( + active_map=active_map, + dem_raster=dem, + extent=extent, + stream_layer=streams, + buffer_radius=buffer_radius, + sampling_interval=sampling_interval, + resolve=True + ) # set watershed size boundaries queries = [ From d1b9a17581e0a665db7f30304f297e770bea2126 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 18:18:05 -0400 Subject: [PATCH 38/42] remove unused imports --- src/FluvialGeomorphology/StreamNetwork.py | 4 ++-- src/TerrainModification/BermAnalysis.py | 2 +- src/TerrainModification/BurnCulverts.py | 2 +- src/TileDrainage/DecisionTree.py | 2 +- src/Utilities/LocalMinimums.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/FluvialGeomorphology/StreamNetwork.py b/src/FluvialGeomorphology/StreamNetwork.py index 42070f4..b297cc1 100644 --- a/src/FluvialGeomorphology/StreamNetwork.py +++ b/src/FluvialGeomorphology/StreamNetwork.py @@ -9,8 +9,8 @@ import arcpy -from ..helpers import license, get_oid, empty_workspace, convert_length, cell_area, reload_module,\ - log, set_required_parameter, raster_and_layer, AREAL_UNITS, AREAL_UNITS_MAP +from ..helpers import license, get_oid, empty_workspace, cell_area, reload_module,\ + log, set_required_parameter, raster_and_layer, AREAL_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate diff --git a/src/TerrainModification/BermAnalysis.py b/src/TerrainModification/BermAnalysis.py index f4e3393..b9b9555 100644 --- a/src/TerrainModification/BermAnalysis.py +++ b/src/TerrainModification/BermAnalysis.py @@ -11,7 +11,7 @@ import arcpy -from ..helpers import license, get_oid, pixel_type, get_z_unit, Z_UNITS, empty_workspace, sanitize, set_required_parameter, reload_module, log, warn, is_empty, raster_and_layer, convert_length +from ..helpers import license, get_oid, pixel_type, get_z_unit, Z_UNITS, empty_workspace, sanitize, set_required_parameter, reload_module, log, warn, is_empty, raster_and_layer from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate diff --git a/src/TerrainModification/BurnCulverts.py b/src/TerrainModification/BurnCulverts.py index 377d670..ee877aa 100644 --- a/src/TerrainModification/BurnCulverts.py +++ b/src/TerrainModification/BurnCulverts.py @@ -9,7 +9,7 @@ import os import arcpy -from ..helpers import license, get_oid, pixel_type, empty_workspace, reload_module, log, raster_and_layer, convert_length +from ..helpers import license, get_oid, pixel_type, empty_workspace, reload_module, log, raster_and_layer from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate diff --git a/src/TileDrainage/DecisionTree.py b/src/TileDrainage/DecisionTree.py index 35674f2..de5ceae 100644 --- a/src/TileDrainage/DecisionTree.py +++ b/src/TileDrainage/DecisionTree.py @@ -8,7 +8,7 @@ import arcpy -from ..helpers import license, get_oid, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS, convert_area +from ..helpers import license, get_oid, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS, ArealUnit from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate diff --git a/src/Utilities/LocalMinimums.py b/src/Utilities/LocalMinimums.py index 9eb6ef2..d91758f 100644 --- a/src/Utilities/LocalMinimums.py +++ b/src/Utilities/LocalMinimums.py @@ -13,7 +13,7 @@ import arcpy -from ..helpers import license, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, convert_length, Z_UNITS +from ..helpers import license, get_z_unit, empty_workspace, reload_module, log, raster_and_layer, Z_UNITS from ..helpers import setup_environment as setup from ..helpers import validate_spatial_reference as validate From 2131d97e5bfdb36ca8f9a4cea3c6ecdc44e9491a Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 20:30:08 -0400 Subject: [PATCH 39/42] doc --- src/helpers/units.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/helpers/units.py b/src/helpers/units.py index 41c2488..956210f 100644 --- a/src/helpers/units.py +++ b/src/helpers/units.py @@ -177,6 +177,7 @@ def to_unit(self: Self, output_unit: LINEAR_UNITS) -> Self: self.unit = output_unit return self def full_unit(self: Self) -> str: + """Return full string description of LINEAR_UNIT stored in LINEAR_UNITS_MAP.""" unit = self.unit for key, value in LINEAR_UNITS_MAP.items(): if value == unit: @@ -245,6 +246,7 @@ def to_unit(self: Self, output_unit: AREAL_UNITS) -> Self: self.unit = output_unit return self def full_unit(self: Self) -> str: + """Return full string description of AREAL_UNIT stored in AREAL_UNITS_MAP.""" unit = self.unit for key, value in AREAL_UNITS_MAP.items(): if value == unit: From d2048434e92ff0837baea7e9fe7cec3915fd1dff Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 20:30:18 -0400 Subject: [PATCH 40/42] rm unused --- src/helpers/geometry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/geometry.py b/src/helpers/geometry.py index fa6eba2..8023415 100644 --- a/src/helpers/geometry.py +++ b/src/helpers/geometry.py @@ -10,7 +10,6 @@ import numpy as np from scipy.spatial import Delaunay from numpy.lib.recfunctions import structured_to_unstructured as stu -from .logging import log # modified from https://github.com/Dan-Patterson/numpy_geometry/blob/master/arcpro_npg/npg/npg/npg_arc_npg.py#L639 def fc_to_numpy_array(in_fc): From 141c76e88e71072d50e489a798985a3af3cd0e2a Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 20:48:54 -0400 Subject: [PATCH 41/42] reconfigure package structure --- SWCD Tools.pyt | 86 ++++++++++++++++++++++++------------------------- src/__init__.py | 9 ------ 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/SWCD Tools.pyt b/SWCD Tools.pyt index 585bfcf..c6fe419 100644 --- a/SWCD Tools.pyt +++ b/SWCD Tools.pyt @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from src import * +from src import AgAssessment, BufferTools, FluvialGeomorphology, Hydrology, TerrainAnalysis, TerrainModification, TileDrainage, Utilities class Toolbox(object): def __init__(self): @@ -10,48 +10,48 @@ class Toolbox(object): self.alias = "SWCD Tools" tools = [ - SlopePolygon, - ContourPolygon, - BurnCulverts, - CollectRasters, - ExportLayouts, - StreamNetwork, - StreamElevation, - GenerateCrossSections, - PolygonCenterline, - LocalMinimums, - RelativeElevationModel, - TopographicPositionIndex, - PointPlots, - ShrubClusters, - BufferPotential, - StreamPowerIndex, - LeastAction, - RunoffCurveNumber, - CalculateEFH2, - WatershedSize, - StreambankDetection, - LandscapePosition, - VBET, - WatershedDelineation, - SubBasinDelineation, - RemoveUnused, - DecisionTree, - ImageDifferencingSetup, - ImageDifferencing, - ImageDifferencingClouds, - TopographicWetness, - PotentialWetlands, - BermAnalysis, - DamRemoval, - DefineParcels, - Agland, - NonAg, - GeocodeAddress, - Forest, - Process, - Export, - Restart + AgAssessment.DefineParcels, + AgAssessment.Agland, + AgAssessment.NonAg, + AgAssessment.Forest, + AgAssessment.Process, + AgAssessment.Export, + AgAssessment.Restart, + BufferTools.PointPlots, + BufferTools.ShrubClusters, + BufferTools.BufferPotential, + FluvialGeomorphology.StreamNetwork, + FluvialGeomorphology.LeastAction, + FluvialGeomorphology.StreambankDetection, + FluvialGeomorphology.StreamElevation, + FluvialGeomorphology.GenerateCrossSections, + FluvialGeomorphology.PolygonCenterline, + Hydrology.CalculateEFH2, + Hydrology.RunoffCurveNumber, + Hydrology.SubBasinDelineation, + Hydrology.WatershedDelineation, + Hydrology.WatershedSize, + TerrainModification.BermAnalysis, + TerrainModification.DamRemoval, + TerrainModification.BurnCulverts, + TerrainAnalysis.StreamPowerIndex, + TerrainAnalysis.LandscapePosition, + TerrainAnalysis.VBET, + TerrainAnalysis.TopographicWetness, + TerrainAnalysis.RelativeElevationModel, + TerrainAnalysis.PotentialWetlands, + TerrainAnalysis.TopographicPositionIndex, + TileDrainage.DecisionTree, + TileDrainage.ImageDifferencingSetup, + TileDrainage.ImageDifferencing, + TileDrainage.ImageDifferencingClouds, + Utilities.SlopePolygon, + Utilities.ContourPolygon, + Utilities.ExportLayouts, + Utilities.LocalMinimums, + Utilities.GeocodeAddress, + Utilities.RemoveUnused, + Utilities.CollectRasters, ] # List of tool classes associated with this toolbox diff --git a/src/__init__.py b/src/__init__.py index 7d8cd65..7c82115 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -5,12 +5,3 @@ # License: Contextual Copyleft AI (CCAI) License v1.0. # Full license in LICENSE file. # -------------------------------------------------------------------------------- - -from .AgAssessment import * -from .BufferTools import * -from .FluvialGeomorphology import * -from .Hydrology import * -from .Utilities import * -from .TerrainAnalysis import * -from .TerrainModification import * -from .TileDrainage import * From 576fb7a82cbfbaa86fa14d46bdd252a8c2e6c4f3 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 16 Jul 2026 20:55:41 -0400 Subject: [PATCH 42/42] rm unused --- src/FluvialGeomorphology/PolygonCenterline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/FluvialGeomorphology/PolygonCenterline.py b/src/FluvialGeomorphology/PolygonCenterline.py index 1ddf0ff..ef40870 100644 --- a/src/FluvialGeomorphology/PolygonCenterline.py +++ b/src/FluvialGeomorphology/PolygonCenterline.py @@ -8,8 +8,6 @@ # Full license in LICENSE file. # -------------------------------------------------------------------------------- -import os -import math import arcpy import numpy as np from scipy.spatial import Delaunay