BERATools 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

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 (153) hide show
  1. beratools/__init__.py +1 -7
  2. beratools/core/algo_centerline.py +491 -351
  3. beratools/core/algo_common.py +497 -0
  4. beratools/core/algo_cost.py +192 -0
  5. beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
  6. beratools/core/algo_footprint_rel.py +577 -0
  7. beratools/core/algo_line_grouping.py +944 -0
  8. beratools/core/algo_merge_lines.py +214 -0
  9. beratools/core/algo_split_with_lines.py +304 -0
  10. beratools/core/algo_tiler.py +428 -0
  11. beratools/core/algo_vertex_optimization.py +469 -0
  12. beratools/core/constants.py +52 -86
  13. beratools/core/logger.py +76 -85
  14. beratools/core/tool_base.py +196 -133
  15. beratools/gui/__init__.py +11 -15
  16. beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
  17. beratools/gui/batch_processing_dlg.py +513 -463
  18. beratools/gui/bt_data.py +481 -487
  19. beratools/gui/bt_gui_main.py +710 -691
  20. beratools/gui/main.py +26 -0
  21. beratools/gui/map_window.py +162 -146
  22. beratools/gui/tool_widgets.py +725 -493
  23. beratools/tools/Beratools_r_script.r +1120 -1120
  24. beratools/tools/Ht_metrics.py +116 -116
  25. beratools/tools/__init__.py +7 -7
  26. beratools/tools/batch_processing.py +136 -132
  27. beratools/tools/canopy_threshold_relative.py +672 -670
  28. beratools/tools/canopycostraster.py +222 -222
  29. beratools/tools/centerline.py +136 -176
  30. beratools/tools/common.py +857 -885
  31. beratools/tools/fl_regen_csf.py +428 -428
  32. beratools/tools/forest_line_attributes.py +408 -408
  33. beratools/tools/line_footprint_absolute.py +213 -363
  34. beratools/tools/line_footprint_fixed.py +436 -282
  35. beratools/tools/line_footprint_functions.py +733 -720
  36. beratools/tools/line_footprint_relative.py +73 -64
  37. beratools/tools/line_grouping.py +45 -0
  38. beratools/tools/ln_relative_metrics.py +615 -615
  39. beratools/tools/r_cal_lpi_elai.r +24 -24
  40. beratools/tools/r_generate_pd_focalraster.r +100 -100
  41. beratools/tools/r_interface.py +79 -79
  42. beratools/tools/r_point_density.r +8 -8
  43. beratools/tools/rpy_chm2trees.py +86 -86
  44. beratools/tools/rpy_dsm_chm_by.py +81 -81
  45. beratools/tools/rpy_dtm_by.py +63 -63
  46. beratools/tools/rpy_find_cellsize.py +43 -43
  47. beratools/tools/rpy_gnd_csf.py +74 -74
  48. beratools/tools/rpy_hummock_hollow.py +85 -85
  49. beratools/tools/rpy_hummock_hollow_raster.py +71 -71
  50. beratools/tools/rpy_las_info.py +51 -51
  51. beratools/tools/rpy_laz2las.py +40 -40
  52. beratools/tools/rpy_lpi_elai_lascat.py +466 -466
  53. beratools/tools/rpy_normalized_lidar_by.py +56 -56
  54. beratools/tools/rpy_percent_above_dbh.py +80 -80
  55. beratools/tools/rpy_points2trees.py +88 -88
  56. beratools/tools/rpy_vegcoverage.py +94 -94
  57. beratools/tools/tiler.py +48 -206
  58. beratools/tools/tool_template.py +69 -54
  59. beratools/tools/vertex_optimization.py +61 -620
  60. beratools/tools/zonal_threshold.py +144 -144
  61. beratools-0.2.1.dist-info/METADATA +109 -0
  62. beratools-0.2.1.dist-info/RECORD +74 -0
  63. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/WHEEL +1 -1
  64. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/licenses/LICENSE +22 -22
  65. beratools/gui/cli.py +0 -18
  66. beratools/gui/gui.json +0 -8
  67. beratools/gui_tk/ASCII Banners.txt +0 -248
  68. beratools/gui_tk/__init__.py +0 -20
  69. beratools/gui_tk/beratools_main.py +0 -515
  70. beratools/gui_tk/bt_widgets.py +0 -442
  71. beratools/gui_tk/cli.py +0 -18
  72. beratools/gui_tk/img/BERALogo.png +0 -0
  73. beratools/gui_tk/img/closed.gif +0 -0
  74. beratools/gui_tk/img/closed.png +0 -0
  75. beratools/gui_tk/img/open.gif +0 -0
  76. beratools/gui_tk/img/open.png +0 -0
  77. beratools/gui_tk/img/tool.gif +0 -0
  78. beratools/gui_tk/img/tool.png +0 -0
  79. beratools/gui_tk/main.py +0 -14
  80. beratools/gui_tk/map_window.py +0 -144
  81. beratools/gui_tk/runner.py +0 -1481
  82. beratools/gui_tk/tooltip.py +0 -55
  83. beratools/third_party/pyqtlet2/__init__.py +0 -9
  84. beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
  85. beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
  86. beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
  87. beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
  88. beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
  89. beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
  90. beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
  91. beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
  92. beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
  93. beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
  94. beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
  95. beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
  96. beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
  97. beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
  98. beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
  99. beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
  100. beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
  101. beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
  102. beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
  103. beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
  104. beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
  105. beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
  106. beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
  107. beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
  108. beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
  109. beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
  110. beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
  111. beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
  112. beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
  113. beratools/third_party/pyqtlet2/mapwidget.py +0 -45
  114. beratools/third_party/pyqtlet2/web/custom.js +0 -43
  115. beratools/third_party/pyqtlet2/web/map.html +0 -23
  116. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
  117. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
  118. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
  119. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
  120. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
  121. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
  122. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
  123. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
  124. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
  125. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
  126. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
  127. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
  128. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
  129. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
  130. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
  131. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
  132. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
  133. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
  134. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
  135. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
  136. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
  137. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
  138. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
  139. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
  140. beratools/tools/forest_line_ecosite.py +0 -216
  141. beratools/tools/lapis_all.py +0 -103
  142. beratools/tools/least_cost_path_from_chm.py +0 -152
  143. beratools-0.2.0.dist-info/METADATA +0 -63
  144. beratools-0.2.0.dist-info/RECORD +0 -142
  145. /beratools/gui/{img → assets}/BERALogo.png +0 -0
  146. /beratools/gui/{img → assets}/closed.gif +0 -0
  147. /beratools/gui/{img → assets}/closed.png +0 -0
  148. /beratools/{gui_tk → gui/assets}/gui.json +0 -0
  149. /beratools/gui/{img → assets}/open.gif +0 -0
  150. /beratools/gui/{img → assets}/open.png +0 -0
  151. /beratools/gui/{img → assets}/tool.gif +0 -0
  152. /beratools/gui/{img → assets}/tool.png +0 -0
  153. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -1,363 +1,213 @@
1
- import time
2
- import itertools
3
-
4
- from line_footprint_functions import *
5
- from common import *
6
-
7
-
8
- def line_footprint(callback, in_line, in_canopy, in_cost, corridor_th_value, max_ln_width,
9
- exp_shk_cell, out_footprint, out_centerline, processes, verbose):
10
- corridor_th_field = 'CorridorTh'
11
- line_seg = gpd.GeoDataFrame.from_file(in_line)
12
- max_ln_width = float(max_ln_width)
13
- exp_shk_cell = int(exp_shk_cell)
14
-
15
- if not compare_crs(vector_crs(in_line), raster_crs(in_canopy)):
16
- print("Line and canopy have different spatial references, please check.")
17
- return
18
-
19
- if not compare_crs(vector_crs(in_line), raster_crs(in_cost)):
20
- print("Line and cost have different spatial references, please check.")
21
- return
22
-
23
- if 'OLnFID' not in line_seg.columns.array:
24
- print("Cannot find 'OLnFID' column in input line data.\n 'OLnFID' will be created")
25
- line_seg['OLnFID'] = line_seg.index
26
-
27
- if 'CorridorTh' not in line_seg.columns.array:
28
- if BT_DEBUGGING:
29
- print("Cannot find 'CorridorTh' column in input line data")
30
- print("New column created: 'CorridorTh")
31
- line_seg['CorridorTh'] = corridor_th_value
32
- else:
33
- corridor_th_value = float(9999999)
34
- if 'OLnSEG' not in line_seg.columns.array:
35
- line_seg['OLnSEG'] = 0
36
-
37
- ori_total_feat = len(line_seg)
38
-
39
- proc_segments = False
40
- if proc_segments:
41
- print("Splitting lines into segments...")
42
- line_seg = split_into_segments(line_seg)
43
- print("Splitting lines into segments... Done")
44
- else:
45
- line_seg = split_into_equal_nth_segments(line_seg)
46
-
47
- line_args = line_prepare(callback, line_seg, in_canopy, in_cost, corridor_th_field, corridor_th_value,
48
- max_ln_width, exp_shk_cell, proc_segments, out_footprint, out_centerline, ori_total_feat)
49
-
50
- # pass single line one at a time for footprint
51
- feat_list = []
52
- footprint_list = []
53
- poly_list = []
54
- centerline_list = []
55
-
56
- process_single_line = process_single_line_segment
57
- if GROUPING_SEGMENT:
58
- process_single_line = process_single_line_whole
59
-
60
- feat_list = execute_multiprocessing(process_single_line, line_args, 'Line footprint',
61
- processes, 1, verbose=verbose)
62
-
63
- print('Generating shapefile ...', flush=True)
64
-
65
- if feat_list:
66
- for i in feat_list:
67
- footprint_list.append(i[0])
68
- poly_list.append(i[1])
69
-
70
- for item in i[2]:
71
- if item:
72
- centerline_list.append(item)
73
-
74
- results = gpd.GeoDataFrame(pd.concat(footprint_list))
75
- results = results.sort_values(by=['OLnFID', 'OLnSEG'])
76
- results = results.reset_index(drop=True)
77
-
78
- # dissolved polygon group by column 'OLnFID'
79
- dissolved_results = results.dissolve(by='OLnFID', as_index=False)
80
- dissolved_results = dissolved_results.drop(columns=['OLnSEG'])
81
- print("Saving output ...", flush=True)
82
- dissolved_results.to_file(out_footprint)
83
-
84
- # detect centerlines
85
- if out_centerline:
86
- # dissolved polygon group by column 'OLnFID'
87
- print("Saving polygons for generating centerlines ...", flush=True)
88
- polys_for_centerline = gpd.GeoDataFrame(pd.concat(poly_list))
89
- polys_for_centerline = polys_for_centerline.dissolve(by='OLnFID', as_index=False)
90
-
91
- # save polygons
92
- path = Path(out_centerline)
93
- path = path.with_stem(path.stem + '_poly')
94
- polys_for_centerline.to_file(path.as_posix())
95
-
96
- centerline_gpd = gpd.GeoDataFrame(geometry=centerline_list, crs=polys_for_centerline.crs)
97
- centerline_gpd.to_file(out_centerline)
98
- print("Centerline file saved", flush=True)
99
-
100
- print(f'%{100}')
101
- print(f'Finishing footprint processing in {time.time() - start_time} seconds')
102
-
103
-
104
- def field_name_list(fc):
105
- # return a list of column name from shapefile
106
- if isinstance(fc, gpd.GeoDataFrame):
107
- field_list = fc.columns.array
108
- else:
109
- field_list = gpd.read_file(fc).columns.array
110
- return field_list
111
-
112
-
113
- def has_field(fc, fi):
114
- # Check column name
115
- field_list = field_name_list(fc)
116
- if fi in field_list:
117
- print("column: {fi} is found")
118
- return True
119
- elif fi == 'CorridorTh':
120
- shapefile = gpd.GeoDataFrame.from_file(fc)
121
- for row in range(0, len(shapefile)):
122
- shapefile.loc[row, fi] = 3.0
123
-
124
- shapefile.to_file(fc)
125
- print("Warning: There is no field named {} in the input data".format('CorridorTh'))
126
- print("Field: 'CorridorTh' is added and default threshold (i.e.3) is adopted")
127
- return True
128
- else:
129
- print("Warning: There is no field named {fi} in the input data")
130
- return False
131
-
132
-
133
- def process_single_line_whole(line):
134
- footprints = []
135
- line_polys = []
136
- centerline_list = []
137
- for line_seg in line:
138
- footprint = process_single_line_segment(line_seg)
139
- if footprint:
140
- footprints.append(footprint[0])
141
- line_polys.append(footprint[1])
142
- centerline_list.append(footprint[2])
143
- else:
144
- print('No footprint or centerline found.')
145
-
146
- polys = None
147
- if line_polys:
148
- polys = pd.concat(line_polys)
149
- polys = polys.dissolve()
150
-
151
- footprint_merge = None
152
- if footprints:
153
- if not all(item is None for item in footprints):
154
- footprint_merge = pd.concat(footprints)
155
- footprint_merge.dissolve()
156
- footprint_merge.drop(columns=['OLnSEG'])
157
- else:
158
- print(f'Empty footprint returned.')
159
-
160
- if len(line) > 0:
161
- print(f"Processing line: {line[0]['OLnFID']}, done.", flush=True)
162
-
163
- return footprint_merge, polys, centerline_list
164
-
165
-
166
- def process_single_line_segment(dict_segment):
167
- # this function takes single line to work the line footprint
168
- # (regardless it process the whole line or individual segment)
169
- in_canopy = dict_segment['in_canopy']
170
- in_cost = dict_segment['in_cost']
171
- corridor_th_value = dict_segment['corridor_th_value']
172
- line_gpd = dict_segment['line_gpd']
173
-
174
- line_id = ''
175
- if 'BT_UID' in dict_segment.keys():
176
- line_id = dict_segment['BT_UID']
177
-
178
- try:
179
- corridor_th_value = float(corridor_th_value)
180
- if corridor_th_value < 0.0:
181
- corridor_th_value = 3.0
182
- except ValueError as e:
183
- print(e)
184
- corridor_th_value = 3.0
185
-
186
- max_ln_width = dict_segment['max_ln_width']
187
- exp_shk_cell = dict_segment['exp_shk_cell']
188
- shapefile_proj = dict_segment['Proj_crs']
189
- original_col_name_list = dict_segment['org_col']
190
-
191
- FID = dict_segment['OLnSEG'] # segment line feature ID
192
- OID = dict_segment['OLnFID'] # original line ID for segment line
193
-
194
- segment_list = []
195
- feat = dict_segment['geometry']
196
- for coord in feat.coords:
197
- segment_list.append(coord)
198
-
199
- # Find origin and destination coordinates
200
- x1, y1 = segment_list[0][0], segment_list[0][1]
201
- x2, y2 = segment_list[-1][0], segment_list[-1][1]
202
-
203
- # Create Point "origin"
204
- origin_point = shapely.Point([x1, y1])
205
- origin = [shapes for shapes in gpd.GeoDataFrame(geometry=[origin_point],
206
- crs=shapefile_proj).geometry]
207
-
208
- # Create Point "destination"
209
- destination_point = shapely.Point([x2, y2])
210
- destination = [shapes for shapes in gpd.GeoDataFrame(geometry=[destination_point],
211
- crs=shapefile_proj).geometry]
212
-
213
- # Buffer around line and clip cost raster and canopy raster
214
- # TODO: deal with NODATA
215
- clip_cost, out_meta = clip_raster(in_cost, feat, max_ln_width)
216
- out_transform = out_meta['transform']
217
- cell_size_x = out_transform[0]
218
- cell_size_y = -out_transform[4]
219
-
220
- if not HAS_COST_RASTER:
221
- clip_cost, clip_canopy = cost_raster(clip_cost, out_meta)
222
- else:
223
- clip_canopy, out_meta = clip_raster(in_canopy, feat, max_ln_width)
224
-
225
- # Work out the corridor from both end of the centerline
226
- try:
227
- if len(clip_canopy.shape) > 2:
228
- clip_canopy = np.squeeze(clip_canopy, axis=0)
229
-
230
- transformer = rasterio.transform.AffineTransformer(out_transform)
231
- source = [transformer.rowcol(x1, y1)]
232
- destination = [transformer.rowcol(x2, y2)]
233
-
234
- corridor_thresh = corridor_raster(clip_cost, out_meta, source, destination,
235
- (cell_size_x, cell_size_y), corridor_th_value)
236
-
237
- def morph_raster(corridor_raster, canopy_raster, exp_shk_cell, cell_size_x):
238
- # Process: Stamp CC and Max Line Width
239
- temp1 = (corridor_thresh + clip_canopy)
240
- raster_class = np.ma.where(temp1 == 0, 1, 0).data
241
-
242
- if exp_shk_cell > 0 and cell_size_x < 1:
243
- # Process: Expand
244
- # FLM original Expand equivalent
245
- cell_size = int(exp_shk_cell * 2 + 1)
246
- expanded = ndimage.grey_dilation(raster_class, size=(cell_size, cell_size))
247
-
248
- # Process: Shrink
249
- # FLM original Shrink equivalent
250
- file_shrink = ndimage.grey_erosion(expanded, size=(cell_size, cell_size))
251
-
252
- else:
253
- if BT_DEBUGGING:
254
- print('No Expand And Shrink cell performed.')
255
- file_shrink = raster_class
256
-
257
- # Process: Boundary Clean
258
- clean_raster = ndimage.gaussian_filter(file_shrink, sigma=0, mode='nearest')
259
-
260
- return clean_raster
261
-
262
- clean_raster = morph_raster(corridor_thresh, in_canopy, exp_shk_cell, cell_size_x)
263
-
264
- # creat mask for non-polygon area
265
- msk = np.where(clean_raster == 1, True, False)
266
-
267
- # Process: ndarray to shapely Polygon
268
- out_polygon = features.shapes(clean_raster, mask=msk, transform=out_transform)
269
-
270
- # create a shapely multipolygon
271
- multi_polygon = []
272
- for shp, value in out_polygon:
273
- multi_polygon.append(shapely.geometry.shape(shp))
274
- poly = shapely.geometry.MultiPolygon(multi_polygon)
275
-
276
- # create a pandas dataframe for the footprint
277
- out_data = pd.DataFrame({'OLnFID': [OID], 'OLnSEG': [FID], 'geometry': poly})
278
- out_gdata = gpd.GeoDataFrame(out_data, geometry='geometry', crs=shapefile_proj)
279
-
280
- if not GROUPING_SEGMENT:
281
- print(f"LP:PSLS: Processing line ID: {dict_segment['OLnSEG']}, done.", flush=True)
282
-
283
- # find contiguous corridor polygon for centerline
284
- corridor_poly_gpd = find_corridor_polygon(corridor_thresh, out_transform, line_gpd)
285
- centerline, status = find_centerline(corridor_poly_gpd.geometry.iloc[0], feat)
286
-
287
- return out_gdata, corridor_poly_gpd, centerline
288
-
289
- except Exception as e:
290
- print(f'Exception: {e}')
291
- return None
292
-
293
-
294
- def line_prepare(callback, line_seg, in_canopy, in_cost, corridor_th_field, corridor_th_value,
295
- max_ln_width, exp_shk_cell, proc_seg, out_footprint, out_centerline, ori_total_feat):
296
- # get the list of original columns names
297
- field_list_col = field_name_list(line_seg)
298
- keep_field_name = []
299
- for col_name in line_seg.columns:
300
- if col_name != 'geometry':
301
- keep_field_name.append(col_name)
302
-
303
- list_of_segment = []
304
-
305
- i = 0
306
- # process when shapefile is not an empty feature class
307
- if len(line_seg) > 0:
308
- for row in range(0, len(line_seg)):
309
- # creates a geometry object
310
- line_gpd = line_seg.loc[[row]]
311
- feat = line_gpd.geometry.iloc[0]
312
- if feat:
313
- feature_attributes = {'seg_length': feat.length, 'geometry': feat,
314
- 'Proj_crs': line_seg.crs, 'line_gpd': line_gpd}
315
-
316
- for col_name in keep_field_name:
317
- feature_attributes[col_name] = line_seg.loc[row, col_name]
318
- list_of_segment.append(feature_attributes)
319
- i += 1
320
-
321
- print(f"There are {ori_total_feat} lines to be processed.")
322
- else:
323
- print("Input line feature is corrupted, exit!")
324
- exit()
325
-
326
- # Add tools arguments into GeoDataFrame record
327
- for record in list_of_segment:
328
- record['in_canopy'] = in_canopy
329
- record['in_cost'] = in_cost
330
- record['corridor_th_field'] = corridor_th_field
331
- record['corridor_th_value'] = record['CorridorTh']
332
- record['max_ln_width'] = max_ln_width
333
- record['exp_shk_cell'] = exp_shk_cell
334
- record['proc_seg'] = proc_seg
335
- record['out_footprint'] = out_footprint
336
- record['out_centerline'] = out_centerline
337
- record['org_col'] = field_list_col
338
-
339
- # TODO: data type changed - return list of GeoDataFrame represents each line or segment
340
- # returns list of list of line attributes, arguments and line gpd
341
- if GROUPING_SEGMENT:
342
- # group line segments by line id
343
- def key_func(x):
344
- return x['OLnFID']
345
-
346
- lines = []
347
-
348
- for key, group in itertools.groupby(list_of_segment, key_func):
349
- lines.append(list(group))
350
-
351
- return lines
352
- else:
353
- return list_of_segment
354
-
355
-
356
- if __name__ == '__main__':
357
- start_time = time.time()
358
- print('Footprint processing started')
359
- print(f'Current time: {time.strftime("%b %Y %H:%M:%S", time.localtime())}')
360
-
361
- in_args, in_verbose = check_arguments()
362
- line_footprint(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
363
- print(f'Current time: {time.strftime("%b %Y %H:%M:%S", time.localtime())}')
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to provide main interface for canopy footprint tool.
14
+ The tool is used to generate the footprint of a line based on absolute threshold.
15
+ """
16
+ import time
17
+
18
+ import geopandas as gpd
19
+ import numpy as np
20
+ import pandas as pd
21
+ import rasterio
22
+ import shapely
23
+
24
+ import beratools.core.algo_centerline as algo_cl
25
+ import beratools.core.algo_common as algo_common
26
+ import beratools.core.algo_cost as algo_cost
27
+ import beratools.core.constants as bt_const
28
+ import beratools.core.tool_base as bt_base
29
+ import beratools.tools.common as bt_common
30
+
31
+
32
+ class FootprintAbsolute:
33
+ """Class to compute the footprint of a line based on absolute threshold."""
34
+
35
+ def __init__(
36
+ self,
37
+ line_seg,
38
+ in_chm,
39
+ corridor_thresh,
40
+ max_ln_width,
41
+ exp_shk_cell,
42
+ ):
43
+ self.line_seg = line_seg
44
+ self.in_chm = in_chm
45
+ self.corridor_thresh = corridor_thresh
46
+ self.max_ln_width = max_ln_width
47
+ self.exp_shk_cell = exp_shk_cell
48
+
49
+ self.footprint = None
50
+ self.corridor_poly_gpd = None
51
+ self.centerline = None
52
+
53
+ def compute(self):
54
+ """Generate line footprint."""
55
+ in_chm = self.in_chm
56
+ corridor_thresh = self.corridor_thresh
57
+ line_gpd = self.line_seg
58
+ max_ln_width = self.max_ln_width
59
+ exp_shk_cell = self.exp_shk_cell
60
+
61
+ try:
62
+ corridor_thresh = float(corridor_thresh)
63
+ if corridor_thresh < 0.0:
64
+ corridor_thresh = 3.0
65
+ except ValueError as e:
66
+ print(f"process_single_line_segment: {e}")
67
+ corridor_thresh = 3.0
68
+
69
+ segment_list = []
70
+ feat = self.line_seg.geometry[0]
71
+ for coord in feat.coords:
72
+ segment_list.append(coord)
73
+
74
+ # Find origin and destination coordinates
75
+ x1, y1 = segment_list[0][0], segment_list[0][1]
76
+ x2, y2 = segment_list[-1][0], segment_list[-1][1]
77
+
78
+ # Buffer around line and clip cost raster and canopy raster
79
+ # TODO: deal with NODATA
80
+ clip_cost, out_meta = bt_common.clip_raster(in_chm, feat, max_ln_width)
81
+ out_transform = out_meta["transform"]
82
+ cell_size_x = out_transform[0]
83
+ cell_size_y = -out_transform[4]
84
+
85
+ clip_cost, clip_canopy = algo_cost.cost_raster(clip_cost, out_meta)
86
+
87
+ # Work out the corridor from both end of the centerline
88
+ if len(clip_canopy.shape) > 2:
89
+ clip_canopy = np.squeeze(clip_canopy, axis=0)
90
+
91
+ transformer = rasterio.transform.AffineTransformer(out_transform)
92
+ source = [transformer.rowcol(x1, y1)]
93
+ destination = [transformer.rowcol(x2, y2)]
94
+
95
+ corridor_thresh = algo_common.corridor_raster(
96
+ clip_cost,
97
+ out_meta,
98
+ source,
99
+ destination,
100
+ (cell_size_x, cell_size_y),
101
+ corridor_thresh,
102
+ )
103
+
104
+ clean_raster = algo_common.morph_raster(
105
+ corridor_thresh, clip_canopy, exp_shk_cell, cell_size_x
106
+ )
107
+
108
+ # create mask for non-polygon area
109
+ msk = np.where(clean_raster == 1, True, False)
110
+ if clean_raster.dtype == np.int64:
111
+ clean_raster = clean_raster.astype(np.int32)
112
+
113
+ # Process: ndarray to shapely Polygon
114
+ out_polygon = rasterio.features.shapes(
115
+ clean_raster, mask=msk, transform=out_transform
116
+ )
117
+
118
+ # create a shapely multipolygon
119
+ multi_polygon = []
120
+ for shp, value in out_polygon:
121
+ multi_polygon.append(shapely.geometry.shape(shp))
122
+ poly = shapely.geometry.MultiPolygon(multi_polygon)
123
+
124
+ # create a pandas dataframe for the footprint
125
+ footprint = gpd.GeoDataFrame(geometry=[poly], crs=self.line_seg.crs)
126
+
127
+ # find contiguous corridor polygon for centerline
128
+ corridor_poly_gpd = algo_cl.find_corridor_polygon(
129
+ corridor_thresh, out_transform, line_gpd
130
+ )
131
+ centerline, status = algo_cl.find_centerline(
132
+ corridor_poly_gpd.geometry.iloc[0], feat
133
+ )
134
+
135
+ self.footprint = footprint
136
+ self.corridor_poly_gpd = corridor_poly_gpd
137
+ self.centerline = centerline
138
+
139
+ def process_single_line(line_footprint):
140
+ line_footprint.compute()
141
+ return line_footprint
142
+
143
+
144
+ def generate_line_class_list(
145
+ in_line,
146
+ in_chm,
147
+ corridor_thresh,
148
+ max_ln_width,
149
+ exp_shk_cell,
150
+ in_layer=None,
151
+ ):
152
+ line_classes = []
153
+ line_list = algo_common.prepare_lines_gdf(in_line, in_layer, proc_segments=False)
154
+
155
+ for line in line_list:
156
+ line_classes.append(
157
+ FootprintAbsolute(line, in_chm, corridor_thresh, max_ln_width, exp_shk_cell)
158
+ )
159
+
160
+ return line_classes
161
+
162
+
163
+ def line_footprint_abs(
164
+ in_line,
165
+ in_chm,
166
+ corridor_thresh,
167
+ max_ln_width,
168
+ exp_shk_cell,
169
+ out_footprint,
170
+ processes,
171
+ verbose,
172
+ in_layer=None,
173
+ out_layer=None,
174
+ parallel_mode=bt_const.ParallelMode.MULTIPROCESSING
175
+ ):
176
+ max_ln_width = float(max_ln_width)
177
+ exp_shk_cell = int(exp_shk_cell)
178
+
179
+ footprint_list = []
180
+ poly_list = []
181
+
182
+ line_class_list = generate_line_class_list(
183
+ in_line, in_chm, corridor_thresh, max_ln_width, exp_shk_cell, in_layer
184
+ )
185
+
186
+ feat_list = bt_base.execute_multiprocessing(
187
+ process_single_line,
188
+ line_class_list,
189
+ "Line footprint",
190
+ processes,
191
+ 1,
192
+ verbose=verbose,
193
+ )
194
+
195
+ if feat_list:
196
+ for i in feat_list:
197
+ footprint_list.append(i.footprint)
198
+ poly_list.append(i.corridor_poly_gpd)
199
+
200
+ results = gpd.GeoDataFrame(pd.concat(footprint_list))
201
+ results = results.reset_index(drop=True)
202
+ results.to_file(out_footprint, layer=out_layer)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ start_time = time.time()
207
+ print("Footprint processing started")
208
+
209
+ in_args, in_verbose = bt_common.check_arguments()
210
+ line_footprint_abs(
211
+ **in_args.input, processes=int(in_args.processes), verbose=in_verbose
212
+ )
213
+ print('Elapsed time: {}'.format(time.time() - start_time))