BERATools 0.3.7__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {beratools-0.3.7 → beratools-0.3.9}/PKG-INFO +4 -1
  2. {beratools-0.3.7 → beratools-0.3.9}/beratools/__init__.py +1 -1
  3. beratools-0.3.9/beratools/core/algo_check_seed_line.py +172 -0
  4. beratools-0.3.9/beratools/core/algo_seed_line_correction.py +606 -0
  5. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_vertex_preclean.py +56 -2
  6. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/canopy_threshold_relative.py +49 -160
  7. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/line_footprint_functions.py +1 -2
  8. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/tool_base.py +1 -1
  9. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/beratools.json +113 -2
  10. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/bt_data.py +23 -0
  11. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/bt_gui_main.py +44 -12
  12. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/tool_widgets.py +107 -33
  13. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/centerline.py +1 -1
  14. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/check_seed_line.py +652 -85
  15. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/line_footprint_relative.py +2 -2
  16. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/vertex_optimization.py +1 -1
  17. {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/spatial_common.py +5 -2
  18. {beratools-0.3.7 → beratools-0.3.9}/pyproject.toml +3 -0
  19. {beratools-0.3.7 → beratools-0.3.9}/.gitignore +0 -0
  20. {beratools-0.3.7 → beratools-0.3.9}/LICENSE +0 -0
  21. {beratools-0.3.7 → beratools-0.3.9}/README.md +0 -0
  22. {beratools-0.3.7 → beratools-0.3.9}/beratools/cli/__init__.py +0 -0
  23. {beratools-0.3.7 → beratools-0.3.9}/beratools/cli/entry.py +0 -0
  24. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/__init__.py +0 -0
  25. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_canopy_footprint_absolute.py +0 -0
  26. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_centerline.py +0 -0
  27. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_common.py +0 -0
  28. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_cost.py +0 -0
  29. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_dijkstra.py +0 -0
  30. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_line_grouping.py +0 -0
  31. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_merge_lines.py +0 -0
  32. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_split_with_lines.py +0 -0
  33. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_vertex_optimization.py +0 -0
  34. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/constants.py +0 -0
  35. {beratools-0.3.7 → beratools-0.3.9}/beratools/core/logger.py +0 -0
  36. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/__init__.py +0 -0
  37. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/LICENSE.md +0 -0
  38. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/UPSTREAM.md +0 -0
  39. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/__init__.py +0 -0
  40. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/_src.py +0 -0
  41. {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/exceptions.py +0 -0
  42. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERALogo.ico +0 -0
  43. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERALogo.png +0 -0
  44. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_GUI_Main.png +0 -0
  45. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_Splash.gif +0 -0
  46. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_WizardImage.png +0 -0
  47. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/closed.gif +0 -0
  48. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/closed.png +0 -0
  49. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/gui.json +0 -0
  50. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/open.gif +0 -0
  51. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/open.png +0 -0
  52. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/tool.gif +0 -0
  53. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/tool.png +0 -0
  54. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/geometry_types.py +0 -0
  55. {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/main.py +0 -0
  56. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/__init__.py +0 -0
  57. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/canopy_footprint_absolute.py +0 -0
  58. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/common.py +0 -0
  59. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/ground_footprint.py +0 -0
  60. {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/tool_template.py +0 -0
  61. {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/env_checks.py +0 -0
  62. {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/tool_args.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: BERATools
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: An advanced forest line feature analysis platform
5
5
  Project-URL: Homepage, https://github.com/appliedgrg/beratools
6
6
  Author-email: AppliedGRG <appliedgrg@gmail.com>, Richard Zeng <richardqzeng@gmail.com>
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
20
20
  Requires-Dist: gdal; platform_system != 'Windows'
21
21
  Requires-Dist: geopandas
22
22
  Requires-Dist: networkit
23
+ Requires-Dist: pandarallel
23
24
  Requires-Dist: pyogrio>=0.9.0
24
25
  Requires-Dist: pyqt5
25
26
  Requires-Dist: rasterio
@@ -49,6 +50,8 @@ Requires-Dist: pymdown-extensions; extra == 'doc'
49
50
  Provides-Extra: test
50
51
  Requires-Dist: gdal==3.8.4; extra == 'test'
51
52
  Requires-Dist: pytest; extra == 'test'
53
+ Requires-Dist: pytest-qt; extra == 'test'
54
+ Requires-Dist: pywinauto; (platform_system == 'Windows') and extra == 'test'
52
55
  Description-Content-Type: text/markdown
53
56
 
54
57
  # BERA Tools
@@ -1,6 +1,6 @@
1
1
  __author__ = """Applied Geospatial Research Group"""
2
2
  __email__ = 'appliedgrg@gmail.com'
3
- __version__ = '0.3.6'
3
+ __version__ = '0.3.9'
4
4
  __license__ = "GPL-3.0-or-later"
5
5
  __copyright__ = "Copyright (c) AppliedGRG"
6
6
  __status__ = "Pre-Alpha"
@@ -0,0 +1,172 @@
1
+ """Per-line vertex preclean helpers for line optimization workflows."""
2
+
3
+ import math
4
+
5
+ import geopandas as gpd
6
+ import shapely.geometry as sh_geom
7
+
8
+ import beratools.core.constants as bt_const
9
+
10
+
11
+ class VertexPrecleaner:
12
+ VALID_MODES = {"full", "endpoint_only"}
13
+
14
+ def __init__(self, close_distance, angle_tol):
15
+ self.close_distance = float(close_distance)
16
+ self.angle_tol = float(angle_tol)
17
+
18
+ @staticmethod
19
+ def _point_distance(coord_a, coord_b):
20
+ return math.hypot(coord_b[0] - coord_a[0], coord_b[1] - coord_a[1])
21
+
22
+ def _bend_score(self, coord_a, coord_b, coord_c):
23
+ v1x = coord_b[0] - coord_a[0]
24
+ v1y = coord_b[1] - coord_a[1]
25
+ v2x = coord_c[0] - coord_b[0]
26
+ v2y = coord_c[1] - coord_b[1]
27
+
28
+ norm1 = math.hypot(v1x, v1y)
29
+ norm2 = math.hypot(v2x, v2y)
30
+ if norm1 <= bt_const.SMALL_BUFFER or norm2 <= bt_const.SMALL_BUFFER:
31
+ return 180.0
32
+
33
+ dot = v1x * v2x + v1y * v2y
34
+ cos_val = max(-1.0, min(1.0, dot / (norm1 * norm2)))
35
+ return math.degrees(math.acos(cos_val))
36
+
37
+ def _shape_loss(self, work_coords, remove_index):
38
+ prev_coord = work_coords[remove_index - 1] if remove_index - 1 >= 0 else None
39
+ curr_coord = work_coords[remove_index]
40
+ next_coord = work_coords[remove_index + 1] if remove_index + 1 < len(work_coords) else None
41
+
42
+ old_scores = []
43
+ if prev_coord is not None and remove_index - 2 >= 0:
44
+ old_scores.append(self._bend_score(work_coords[remove_index - 2], prev_coord, curr_coord))
45
+ if next_coord is not None and remove_index + 2 < len(work_coords):
46
+ old_scores.append(self._bend_score(curr_coord, next_coord, work_coords[remove_index + 2]))
47
+ if prev_coord is not None and next_coord is not None:
48
+ old_scores.append(self._bend_score(prev_coord, curr_coord, next_coord))
49
+
50
+ new_scores = []
51
+ if prev_coord is not None and next_coord is not None:
52
+ if remove_index - 2 >= 0:
53
+ new_scores.append(self._bend_score(work_coords[remove_index - 2], prev_coord, next_coord))
54
+ if remove_index + 2 < len(work_coords):
55
+ new_scores.append(self._bend_score(prev_coord, next_coord, work_coords[remove_index + 2]))
56
+
57
+ old_peak = max(old_scores) if old_scores else 0.0
58
+ new_peak = max(new_scores) if new_scores else 0.0
59
+ significant_penalty = 50.0 if old_peak > self.angle_tol and new_peak <= self.angle_tol else 0.0
60
+ return abs(old_peak - new_peak) + significant_penalty
61
+
62
+ def _dedupe_consecutive_coords(self, coords):
63
+ if not coords:
64
+ return []
65
+
66
+ work_coords = [coords[0]]
67
+ for coord in coords[1:]:
68
+ if self._point_distance(work_coords[-1], coord) > bt_const.SMALL_BUFFER:
69
+ work_coords.append(coord)
70
+
71
+ return work_coords
72
+
73
+ def _finalize_line(self, coords, original_line):
74
+ if len(coords) < 2:
75
+ return None
76
+
77
+ if self._point_distance(coords[0], coords[-1]) <= bt_const.SMALL_BUFFER:
78
+ return None
79
+
80
+ if len(coords) == len(original_line.coords):
81
+ return original_line
82
+
83
+ return sh_geom.LineString(coords)
84
+
85
+ def _cleanup_endpoint_only(self, coords):
86
+ while len(coords) > 2 and self._point_distance(coords[0], coords[1]) < self.close_distance:
87
+ coords.pop(1)
88
+
89
+ while len(coords) > 2 and self._point_distance(coords[-2], coords[-1]) < self.close_distance:
90
+ coords.pop(-2)
91
+
92
+ return coords
93
+
94
+ def cleanup_line(self, line, mode="full"):
95
+ if mode not in self.VALID_MODES:
96
+ raise ValueError(f"Unsupported vertex cleanup mode: {mode}")
97
+
98
+ coords = [tuple(coord) for coord in line.coords]
99
+ if len(coords) <= 2:
100
+ return line
101
+
102
+ work_coords = self._dedupe_consecutive_coords(coords)
103
+ if len(work_coords) <= 2:
104
+ return self._finalize_line(work_coords, line)
105
+
106
+ work_coords = self._cleanup_endpoint_only(work_coords)
107
+ if mode == "endpoint_only":
108
+ return self._finalize_line(work_coords, line)
109
+
110
+ i = 1
111
+ while i < len(work_coords) - 2:
112
+ left_coord = work_coords[i]
113
+ right_coord = work_coords[i + 1]
114
+ if self._point_distance(left_coord, right_coord) >= self.close_distance:
115
+ i += 1
116
+ continue
117
+
118
+ left_score = self._bend_score(work_coords[i - 1], left_coord, right_coord)
119
+ right_score = self._bend_score(left_coord, right_coord, work_coords[i + 2])
120
+
121
+ if left_score > self.angle_tol and right_score <= self.angle_tol:
122
+ remove_index = i + 1
123
+ elif right_score > self.angle_tol and left_score <= self.angle_tol:
124
+ remove_index = i
125
+ else:
126
+ remove_left_loss = self._shape_loss(work_coords, i)
127
+ remove_right_loss = self._shape_loss(work_coords, i + 1)
128
+ remove_index = i if remove_left_loss <= remove_right_loss else i + 1
129
+
130
+ work_coords.pop(remove_index)
131
+ if i > 1:
132
+ i -= 1
133
+
134
+ return self._finalize_line(work_coords, line)
135
+
136
+ def remove_close_vertices(self, gdf, mode="full"):
137
+ if mode not in self.VALID_MODES:
138
+ raise ValueError(f"Unsupported vertex cleanup mode: {mode}")
139
+
140
+ if gdf is None or gdf.empty:
141
+ return gdf
142
+
143
+ if isinstance(gdf, gpd.GeoSeries):
144
+ gdf = gpd.GeoDataFrame(geometry=gdf)
145
+
146
+ out_gdf = gdf.copy()
147
+
148
+ if any(getattr(geom, "geom_type", None) == "MultiLineString" for geom in out_gdf.geometry):
149
+ out_gdf = out_gdf.explode(index_parts=False)
150
+
151
+ cleaned_geoms = []
152
+ for geom in out_gdf.geometry:
153
+ if geom is None or geom.is_empty:
154
+ cleaned_geoms.append(None)
155
+ continue
156
+
157
+ if geom.geom_type != "LineString":
158
+ cleaned_geoms.append(geom)
159
+ continue
160
+
161
+ cleaned_geoms.append(self.cleanup_line(geom, mode=mode))
162
+
163
+ geom_series = gpd.GeoSeries(cleaned_geoms, index=out_gdf.index, crs=out_gdf.crs)
164
+ valid_mask = geom_series.notna() & ~geom_series.is_empty
165
+ out_gdf = out_gdf.loc[valid_mask].copy()
166
+ out_gdf.set_geometry(geom_series.loc[valid_mask], inplace=True)
167
+ out_gdf = out_gdf[out_gdf.geometry.length > bt_const.SMALL_BUFFER]
168
+ out_gdf = gpd.GeoDataFrame(out_gdf, crs=gdf.crs)
169
+ out_gdf.reset_index(drop=True, inplace=True)
170
+
171
+ return out_gdf
172
+