BERATools 0.2.3__py3-none-any.whl → 0.2.5__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.
- beratools/__init__.py +8 -3
- beratools/core/{algo_footprint_rel.py → algo_canopy_footprint_exp.py} +176 -139
- beratools/core/algo_centerline.py +61 -77
- beratools/core/algo_common.py +48 -57
- beratools/core/algo_cost.py +18 -25
- beratools/core/algo_dijkstra.py +37 -45
- beratools/core/algo_line_grouping.py +100 -100
- beratools/core/algo_merge_lines.py +40 -8
- beratools/core/algo_split_with_lines.py +289 -304
- beratools/core/algo_vertex_optimization.py +25 -46
- beratools/core/canopy_threshold_relative.py +755 -0
- beratools/core/constants.py +8 -9
- beratools/{tools → core}/line_footprint_functions.py +411 -258
- beratools/core/logger.py +18 -2
- beratools/core/tool_base.py +17 -75
- beratools/gui/assets/BERALogo.ico +0 -0
- beratools/gui/assets/BERA_Splash.gif +0 -0
- beratools/gui/assets/BERA_WizardImage.png +0 -0
- beratools/gui/assets/beratools.json +475 -2171
- beratools/gui/bt_data.py +585 -234
- beratools/gui/bt_gui_main.py +129 -91
- beratools/gui/main.py +4 -7
- beratools/gui/tool_widgets.py +530 -354
- beratools/tools/__init__.py +0 -7
- beratools/tools/{line_footprint_absolute.py → canopy_footprint_absolute.py} +81 -56
- beratools/tools/canopy_footprint_exp.py +113 -0
- beratools/tools/centerline.py +30 -37
- beratools/tools/check_seed_line.py +127 -0
- beratools/tools/common.py +65 -586
- beratools/tools/{line_footprint_fixed.py → ground_footprint.py} +140 -117
- beratools/tools/line_footprint_relative.py +64 -35
- beratools/tools/tool_template.py +48 -40
- beratools/tools/vertex_optimization.py +20 -34
- beratools/utility/env_checks.py +53 -0
- beratools/utility/spatial_common.py +210 -0
- beratools/utility/tool_args.py +138 -0
- beratools-0.2.5.dist-info/METADATA +134 -0
- beratools-0.2.5.dist-info/RECORD +50 -0
- {beratools-0.2.3.dist-info → beratools-0.2.5.dist-info}/WHEEL +1 -1
- beratools-0.2.5.dist-info/entry_points.txt +3 -0
- beratools-0.2.5.dist-info/licenses/LICENSE +674 -0
- beratools/core/algo_tiler.py +0 -428
- beratools/gui/__init__.py +0 -11
- beratools/gui/batch_processing_dlg.py +0 -513
- beratools/gui/map_window.py +0 -162
- beratools/tools/Beratools_r_script.r +0 -1120
- beratools/tools/Ht_metrics.py +0 -116
- beratools/tools/batch_processing.py +0 -136
- beratools/tools/canopy_threshold_relative.py +0 -672
- beratools/tools/canopycostraster.py +0 -222
- beratools/tools/fl_regen_csf.py +0 -428
- beratools/tools/forest_line_attributes.py +0 -408
- beratools/tools/line_grouping.py +0 -45
- beratools/tools/ln_relative_metrics.py +0 -615
- beratools/tools/r_cal_lpi_elai.r +0 -25
- beratools/tools/r_generate_pd_focalraster.r +0 -101
- beratools/tools/r_interface.py +0 -80
- beratools/tools/r_point_density.r +0 -9
- beratools/tools/rpy_chm2trees.py +0 -86
- beratools/tools/rpy_dsm_chm_by.py +0 -81
- beratools/tools/rpy_dtm_by.py +0 -63
- beratools/tools/rpy_find_cellsize.py +0 -43
- beratools/tools/rpy_gnd_csf.py +0 -74
- beratools/tools/rpy_hummock_hollow.py +0 -85
- beratools/tools/rpy_hummock_hollow_raster.py +0 -71
- beratools/tools/rpy_las_info.py +0 -51
- beratools/tools/rpy_laz2las.py +0 -40
- beratools/tools/rpy_lpi_elai_lascat.py +0 -466
- beratools/tools/rpy_normalized_lidar_by.py +0 -56
- beratools/tools/rpy_percent_above_dbh.py +0 -80
- beratools/tools/rpy_points2trees.py +0 -88
- beratools/tools/rpy_vegcoverage.py +0 -94
- beratools/tools/tiler.py +0 -48
- beratools/tools/zonal_threshold.py +0 -144
- beratools-0.2.3.dist-info/METADATA +0 -108
- beratools-0.2.3.dist-info/RECORD +0 -74
- beratools-0.2.3.dist-info/entry_points.txt +0 -2
- beratools-0.2.3.dist-info/licenses/LICENSE +0 -22
|
@@ -13,49 +13,35 @@ Description:
|
|
|
13
13
|
The purpose of this script is the public interface for vertex optimization.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
import
|
|
16
|
+
import logging
|
|
17
17
|
|
|
18
18
|
import beratools.core.algo_vertex_optimization as bt_vo
|
|
19
|
-
import beratools.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
in_layer=
|
|
31
|
-
|
|
32
|
-
):
|
|
33
|
-
if not bt_common.compare_crs(
|
|
34
|
-
bt_common.vector_crs(in_line), bt_common.raster_crs(in_raster)
|
|
35
|
-
):
|
|
19
|
+
import beratools.utility.spatial_common as sp_common
|
|
20
|
+
from beratools.core.logger import Logger
|
|
21
|
+
from beratools.utility.tool_args import CallMode
|
|
22
|
+
|
|
23
|
+
log = Logger("vertex_optimization", file_level=logging.INFO)
|
|
24
|
+
logger = log.get_logger()
|
|
25
|
+
print = log.print
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def vertex_optimization(in_line, in_raster, search_distance, line_radius, out_line,
|
|
29
|
+
processes=0, call_mode=CallMode.CLI, log_level="INFO"):
|
|
30
|
+
in_file, in_layer = sp_common.decode_file_layer(in_line)
|
|
31
|
+
if not sp_common.compare_crs(sp_common.vector_crs(in_file), sp_common.raster_crs(in_raster)):
|
|
36
32
|
return
|
|
37
33
|
|
|
38
|
-
vg = bt_vo.VertexGrouping(
|
|
39
|
-
in_line,
|
|
40
|
-
in_raster,
|
|
41
|
-
search_distance,
|
|
42
|
-
line_radius,
|
|
43
|
-
out_line,
|
|
44
|
-
processes,
|
|
45
|
-
verbose,
|
|
46
|
-
in_layer,
|
|
47
|
-
out_layer,
|
|
48
|
-
)
|
|
34
|
+
vg = bt_vo.VertexGrouping(in_file, in_raster, search_distance, line_radius, processes, call_mode, layer=in_layer)
|
|
49
35
|
vg.create_all_vertex_groups()
|
|
50
36
|
vg.compute()
|
|
51
37
|
vg.update_all_lines()
|
|
52
38
|
vg.save_all_layers(out_line)
|
|
53
39
|
|
|
54
|
-
|
|
55
40
|
if __name__ == "__main__":
|
|
56
|
-
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
from beratools.utility.tool_args import compose_tool_kwargs
|
|
57
44
|
start_time = time.time()
|
|
58
|
-
vertex_optimization
|
|
59
|
-
|
|
60
|
-
)
|
|
45
|
+
kwargs = compose_tool_kwargs("vertex_optimization")
|
|
46
|
+
vertex_optimization(**kwargs)
|
|
61
47
|
print("Elapsed time: {}".format(time.time() - start_time))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Environment validation helpers for BERA Tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_gdal_proj_env_warning() -> str | None:
|
|
12
|
+
"""Return a Windows-only GDAL/PROJ environment warning message, or None."""
|
|
13
|
+
if not sys.platform.startswith("win"):
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
gdal_data_env = os.environ.get("GDAL_DATA")
|
|
17
|
+
proj_lib_env = os.environ.get("PROJ_LIB")
|
|
18
|
+
if not (gdal_data_env or proj_lib_env):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
prefix_path = Path(sys.prefix).resolve()
|
|
22
|
+
mismatches: list[tuple[str, str]] = []
|
|
23
|
+
|
|
24
|
+
if gdal_data_env:
|
|
25
|
+
gdal_path = Path(gdal_data_env).resolve()
|
|
26
|
+
if not gdal_path.is_relative_to(prefix_path):
|
|
27
|
+
mismatches.append(("GDAL_DATA", gdal_data_env))
|
|
28
|
+
|
|
29
|
+
if proj_lib_env:
|
|
30
|
+
proj_path = Path(proj_lib_env).resolve()
|
|
31
|
+
if not proj_path.is_relative_to(prefix_path):
|
|
32
|
+
mismatches.append(("PROJ_LIB", proj_lib_env))
|
|
33
|
+
|
|
34
|
+
if not mismatches:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
mismatch_details = "\n".join(
|
|
38
|
+
f"{env_name:<9}: {env_value}" for env_name, env_value in mismatches
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
"[GDAL/PROJ Warning] Potential env mismatch\n"
|
|
43
|
+
f"Python env: {sys.prefix}\n"
|
|
44
|
+
f"{mismatch_details}\n"
|
|
45
|
+
"If you encounter GDAL/PROJ errors, ensure these env paths belong to this Python environment."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def warn_gdal_proj_env() -> None:
|
|
50
|
+
"""Emit a warning for CLI/headless usage if GDAL/PROJ env vars look mismatched."""
|
|
51
|
+
warning_message = get_gdal_proj_env_warning()
|
|
52
|
+
if warning_message:
|
|
53
|
+
warnings.warn(warning_message)
|
|
@@ -0,0 +1,210 @@
|
|
|
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, Maverick Fong
|
|
8
|
+
|
|
9
|
+
Description:
|
|
10
|
+
This script is part of the BERA Tools.
|
|
11
|
+
Webpage: https://github.com/appliedgrg/beratools
|
|
12
|
+
|
|
13
|
+
This file is intended to be hosting common spatial classes/functions for BERA Tools
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
import geopandas as gpd
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pyproj
|
|
21
|
+
import rasterio
|
|
22
|
+
from osgeo import gdal, osr, version_info
|
|
23
|
+
from pyogrio import set_gdal_config_options
|
|
24
|
+
from rasterio import mask
|
|
25
|
+
|
|
26
|
+
import beratools.core.constants as bt_const
|
|
27
|
+
|
|
28
|
+
# suppress pandas UserWarning: Geometry column contains no geometry when splitting lines
|
|
29
|
+
warnings.simplefilter(action="ignore", category=UserWarning)
|
|
30
|
+
|
|
31
|
+
# restore .shx for shapefile for using GDAL or pyogrio
|
|
32
|
+
gdal.SetConfigOption("SHAPE_RESTORE_SHX", "YES")
|
|
33
|
+
set_gdal_config_options({"SHAPE_RESTORE_SHX": "YES"}) # for pyogrio
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# suppress all kinds of warnings
|
|
37
|
+
if not bt_const.BT_DEBUGGING:
|
|
38
|
+
gdal.SetConfigOption("CPL_LOG", "NUL") # GDAL warning
|
|
39
|
+
warnings.filterwarnings("ignore") # suppress warnings
|
|
40
|
+
warnings.simplefilter(action="ignore", category=UserWarning) # suppress Pandas UserWarning
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def clip_raster(
|
|
44
|
+
in_raster_file,
|
|
45
|
+
clip_geom,
|
|
46
|
+
buffer=0.0,
|
|
47
|
+
out_raster_file=None,
|
|
48
|
+
default_nodata=bt_const.BT_NODATA,
|
|
49
|
+
):
|
|
50
|
+
out_meta = None
|
|
51
|
+
with rasterio.open(in_raster_file) as raster_file:
|
|
52
|
+
out_meta = raster_file.meta
|
|
53
|
+
ras_nodata = out_meta["nodata"]
|
|
54
|
+
if ras_nodata is None:
|
|
55
|
+
ras_nodata = default_nodata
|
|
56
|
+
|
|
57
|
+
clip_geo_buffer = [clip_geom.buffer(buffer)]
|
|
58
|
+
out_image = None
|
|
59
|
+
out_image, out_transform = mask.mask(
|
|
60
|
+
raster_file, clip_geo_buffer, crop=True, nodata=ras_nodata, filled=True
|
|
61
|
+
)
|
|
62
|
+
if np.isnan(ras_nodata):
|
|
63
|
+
out_image[np.isnan(out_image)] = default_nodata
|
|
64
|
+
|
|
65
|
+
elif np.isinf(ras_nodata):
|
|
66
|
+
out_image[np.isinf(out_image)] = default_nodata
|
|
67
|
+
else:
|
|
68
|
+
out_image[out_image == ras_nodata] = default_nodata
|
|
69
|
+
|
|
70
|
+
out_image = np.ma.masked_where(out_image == default_nodata, out_image)
|
|
71
|
+
out_image.fill_value = default_nodata
|
|
72
|
+
ras_nodata = default_nodata
|
|
73
|
+
|
|
74
|
+
height, width = out_image.shape[1:]
|
|
75
|
+
|
|
76
|
+
out_meta.update(
|
|
77
|
+
{
|
|
78
|
+
"driver": "GTiff",
|
|
79
|
+
"height": height,
|
|
80
|
+
"width": width,
|
|
81
|
+
"transform": out_transform,
|
|
82
|
+
"nodata": ras_nodata,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if out_raster_file:
|
|
87
|
+
with rasterio.open(out_raster_file, "w", **out_meta) as dest:
|
|
88
|
+
dest.write(out_image)
|
|
89
|
+
print("[Clip raster]: data saved to {}.".format(out_raster_file))
|
|
90
|
+
|
|
91
|
+
return out_image, out_meta
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def decode_file_layer(encoded):
|
|
95
|
+
"""
|
|
96
|
+
Decode encoded file|layer string into file path and layer name.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
encoded (str): Encoded string like "file.shp|" or "C:/path/file.gpkg|layer"
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
tuple: (file_path, layer_name) where layer_name is None if empty
|
|
103
|
+
"""
|
|
104
|
+
if "|" in encoded:
|
|
105
|
+
file_path, layer = encoded.rsplit("|", 1)
|
|
106
|
+
layer_name = layer if layer else None
|
|
107
|
+
elif "::" in encoded:
|
|
108
|
+
file_path, layer = encoded.rsplit("::", 1)
|
|
109
|
+
layer_name = layer if layer else None
|
|
110
|
+
else:
|
|
111
|
+
file_path = encoded
|
|
112
|
+
layer_name = None
|
|
113
|
+
return file_path, layer_name
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def vector_crs(in_vector):
|
|
117
|
+
osr_crs = osr.SpatialReference()
|
|
118
|
+
from pyproj.enums import WktVersion
|
|
119
|
+
|
|
120
|
+
vec_crs = None
|
|
121
|
+
# open input vector data as GeoDataFrame
|
|
122
|
+
gpd_vector = gpd.GeoDataFrame.from_file(in_vector)
|
|
123
|
+
try:
|
|
124
|
+
if gpd_vector.crs is not None:
|
|
125
|
+
vec_crs = gpd_vector.crs
|
|
126
|
+
if version_info.major < 3:
|
|
127
|
+
osr_crs.ImportFromWkt(vec_crs.to_wkt(WktVersion.WKT1_GDAL))
|
|
128
|
+
else:
|
|
129
|
+
osr_crs.ImportFromEPSG(vec_crs.to_epsg())
|
|
130
|
+
return osr_crs
|
|
131
|
+
else:
|
|
132
|
+
print("No CRS found in the input feature, please check!")
|
|
133
|
+
exit()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(e)
|
|
136
|
+
exit()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def raster_crs(in_raster):
|
|
140
|
+
osr_crs = osr.SpatialReference()
|
|
141
|
+
with rasterio.open(in_raster) as raster_file:
|
|
142
|
+
from pyproj.enums import WktVersion
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if raster_file.crs is not None:
|
|
146
|
+
vec_crs = raster_file.crs
|
|
147
|
+
if version_info.major < 3:
|
|
148
|
+
osr_crs.ImportFromWkt(vec_crs.to_wkt(WktVersion.WKT1_GDAL))
|
|
149
|
+
else:
|
|
150
|
+
osr_crs.ImportFromEPSG(vec_crs.to_epsg())
|
|
151
|
+
return osr_crs
|
|
152
|
+
else:
|
|
153
|
+
print("No Coordinate Reference System (CRS) find in the input feature, please check!")
|
|
154
|
+
exit()
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(e)
|
|
157
|
+
exit()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_crs_proj_name(crs_norm, label="crs"):
|
|
161
|
+
import warnings
|
|
162
|
+
|
|
163
|
+
if crs_norm.is_compound:
|
|
164
|
+
op = crs_norm.sub_crs_list[0].coordinate_operation
|
|
165
|
+
if op is None:
|
|
166
|
+
warnings.warn(f"{label}.sub_crs_list[0].coordinate_operation is None; using 'unknown'")
|
|
167
|
+
return "unknown"
|
|
168
|
+
return op.name
|
|
169
|
+
elif crs_norm.name == "unnamed":
|
|
170
|
+
return None
|
|
171
|
+
else:
|
|
172
|
+
op = crs_norm.coordinate_operation
|
|
173
|
+
if op is None:
|
|
174
|
+
warnings.warn(f"{label}.coordinate_operation is None; using 'unknown'")
|
|
175
|
+
return "unknown"
|
|
176
|
+
return op.name
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def compare_crs(crs_org, crs_dst):
|
|
180
|
+
if crs_org and crs_dst:
|
|
181
|
+
if crs_org.IsSameGeogCS(crs_dst):
|
|
182
|
+
print("Check: Input file Spatial Reference are the same, continue.")
|
|
183
|
+
return True
|
|
184
|
+
else:
|
|
185
|
+
crs_org_norm = pyproj.CRS(crs_org.ExportToWkt())
|
|
186
|
+
crs_dst_norm = pyproj.CRS(crs_dst.ExportToWkt())
|
|
187
|
+
|
|
188
|
+
crs_org_proj = get_crs_proj_name(crs_org_norm, "crs_org_norm")
|
|
189
|
+
if crs_org_proj is None:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
crs_dst_proj = get_crs_proj_name(crs_dst_norm, "crs_dst_norm")
|
|
193
|
+
if crs_dst_proj is None:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
if crs_org_proj == crs_dst_proj:
|
|
197
|
+
if crs_org_norm.name == crs_dst_norm.name:
|
|
198
|
+
print("Input files Spatial Reference are the same, continue.")
|
|
199
|
+
return True
|
|
200
|
+
else:
|
|
201
|
+
print(
|
|
202
|
+
"""Checked: Data are on the same projected Zone but using
|
|
203
|
+
different Spatial Reference. \n Consider to re-project
|
|
204
|
+
all data onto same spatial reference system.\n Process Stop."""
|
|
205
|
+
)
|
|
206
|
+
exit()
|
|
207
|
+
else:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
return False
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Utility functions for tool argument parsing."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# TODO: Consider using StrEnum from Python 3.11+
|
|
12
|
+
class StrEnum(str, Enum):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class CallMode(StrEnum):
|
|
16
|
+
CLI = "CLI"
|
|
17
|
+
GUI = "GUI"
|
|
18
|
+
|
|
19
|
+
def determine_cpu_core_limit():
|
|
20
|
+
running_windows = platform.system() == "Windows"
|
|
21
|
+
core_count = os.cpu_count() or 1
|
|
22
|
+
max_cores = max(core_count - 1, 1)
|
|
23
|
+
if running_windows:
|
|
24
|
+
max_cores = min(max_cores, 60)
|
|
25
|
+
return max_cores
|
|
26
|
+
|
|
27
|
+
def compose_tool_kwargs(tool_api):
|
|
28
|
+
"""
|
|
29
|
+
Auto-detecting argument composer for both CLI and GUI modes.
|
|
30
|
+
|
|
31
|
+
- Detects mode from --input presence
|
|
32
|
+
- GUI: unpacks --input JSON, adds common args from CLI flags
|
|
33
|
+
- CLI: parses all args from beratools.json definitions with validation
|
|
34
|
+
- Returns: complete kwargs dict with all parameters
|
|
35
|
+
|
|
36
|
+
Note: In CLI mode, argparse automatically prints help and exits
|
|
37
|
+
if required arguments are missing.
|
|
38
|
+
"""
|
|
39
|
+
from beratools.gui.bt_data import BTData
|
|
40
|
+
|
|
41
|
+
bt_data = BTData()
|
|
42
|
+
tool_name = bt_data.get_bera_tool_name(tool_api)
|
|
43
|
+
tool_api_name = tool_api
|
|
44
|
+
|
|
45
|
+
mode_parser = argparse.ArgumentParser(add_help=False)
|
|
46
|
+
mode_parser.add_argument("-i", "--input", type=json.loads, default=None)
|
|
47
|
+
mode_parser.add_argument("-c", "--call_mode", default=CallMode.CLI.value)
|
|
48
|
+
mode_parser.add_argument("-p", "--processes", type=int, default=0)
|
|
49
|
+
mode_parser.add_argument("-l", "--log_level", default="INFO")
|
|
50
|
+
mode_args, remaining = mode_parser.parse_known_args()
|
|
51
|
+
|
|
52
|
+
if mode_args.input is None:
|
|
53
|
+
from beratools.utility import env_checks
|
|
54
|
+
|
|
55
|
+
env_checks.warn_gdal_proj_env()
|
|
56
|
+
|
|
57
|
+
if mode_args.input is not None:
|
|
58
|
+
# GUI MODE: unpack tool params from JSON with TYPE VALIDATION
|
|
59
|
+
try:
|
|
60
|
+
kwargs = bt_data.validate_tool_params(tool_name, mode_args.input)
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
print(f"ERROR: {str(e)}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
# CLI MODE: parse tool params from CLI args with validation
|
|
67
|
+
# Load parameter definitions from beratools.json
|
|
68
|
+
tool_params = bt_data.get_bera_tool_params(tool_name)
|
|
69
|
+
|
|
70
|
+
# Helper: Separate parameters by type
|
|
71
|
+
required_params = [
|
|
72
|
+
p for p in tool_params.get("parameters", []) if p.get("variable") and not p.get("optional", False)
|
|
73
|
+
]
|
|
74
|
+
optional_params = [
|
|
75
|
+
p for p in tool_params.get("parameters", []) if p.get("variable") and p.get("optional", False)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Build usage string with only required parameters
|
|
79
|
+
required_flags = " ".join([f"--{p.get('variable')}" for p in required_params])
|
|
80
|
+
custom_usage = f"python {tool_api_name}.py {required_flags}" if required_flags else f"python {tool_api_name}.py"
|
|
81
|
+
|
|
82
|
+
full_parser = argparse.ArgumentParser(
|
|
83
|
+
prog=tool_name,
|
|
84
|
+
usage=custom_usage,
|
|
85
|
+
description=f"Tool: {tool_name}",
|
|
86
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create argument groups
|
|
90
|
+
required_group = full_parser.add_argument_group("Required Parameters")
|
|
91
|
+
optional_group = full_parser.add_argument_group("Optional Parameters")
|
|
92
|
+
framework_group = full_parser.add_argument_group("Framework Options")
|
|
93
|
+
|
|
94
|
+
# Helper function to add parameters to argument group
|
|
95
|
+
def add_param_args(group, params, required=False):
|
|
96
|
+
"""Add parameter arguments to argument group."""
|
|
97
|
+
for param in params:
|
|
98
|
+
arg_config = {"required": required}
|
|
99
|
+
if param.get("default_value") is not None and param.get("default_value") != "":
|
|
100
|
+
arg_config["default"] = param.get("default_value")
|
|
101
|
+
if param.get("description"):
|
|
102
|
+
# Escape % in help text to prevent argparse formatting errors
|
|
103
|
+
arg_config["help"] = param.get("description").replace("%", "%%")
|
|
104
|
+
group.add_argument(f"--{param.get('variable')}", **arg_config)
|
|
105
|
+
|
|
106
|
+
# Add required and optional parameters using helper
|
|
107
|
+
add_param_args(required_group, required_params, required=True)
|
|
108
|
+
add_param_args(optional_group, optional_params, required=False)
|
|
109
|
+
|
|
110
|
+
# Add framework parameters to their group (with descriptions)
|
|
111
|
+
framework_group.add_argument(
|
|
112
|
+
"-p",
|
|
113
|
+
"--processes",
|
|
114
|
+
type=int,
|
|
115
|
+
default=mode_args.processes,
|
|
116
|
+
help="Number of CPU processes (default: 1)",
|
|
117
|
+
)
|
|
118
|
+
framework_group.add_argument(
|
|
119
|
+
"-c", "--call_mode", default=mode_args.call_mode, help="'CLI' or 'GUI' mode (default: CLI)"
|
|
120
|
+
)
|
|
121
|
+
framework_group.add_argument(
|
|
122
|
+
"-l",
|
|
123
|
+
"--log_level",
|
|
124
|
+
default=mode_args.log_level,
|
|
125
|
+
help="DEBUG, INFO, WARNING, ERROR (default: INFO)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Parse all arguments
|
|
129
|
+
# This automatically prints help and exits if required args are missing
|
|
130
|
+
args = full_parser.parse_args()
|
|
131
|
+
kwargs = vars(args)
|
|
132
|
+
|
|
133
|
+
# Always ensure framework params are set from first pass
|
|
134
|
+
kwargs.update(
|
|
135
|
+
{"processes": mode_args.processes, "call_mode": mode_args.call_mode, "log_level": mode_args.log_level}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return kwargs
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: BERATools
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: An advanced forest line feature analysis platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/appliedgrg/beratools
|
|
6
|
+
Author-email: AppliedGRG <appliedgrg@gmail.com>, Richard Zeng <richardqzeng@gmail.com>
|
|
7
|
+
License: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: BERA,Line
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: bera-centerlines
|
|
21
|
+
Requires-Dist: gdal; platform_system != 'Windows'
|
|
22
|
+
Requires-Dist: geopandas
|
|
23
|
+
Requires-Dist: networkit
|
|
24
|
+
Requires-Dist: pyogrio>=0.9.0
|
|
25
|
+
Requires-Dist: pyqt5
|
|
26
|
+
Requires-Dist: rasterio
|
|
27
|
+
Requires-Dist: scikit-image>=0.24.0
|
|
28
|
+
Requires-Dist: tabulate
|
|
29
|
+
Requires-Dist: tqdm
|
|
30
|
+
Requires-Dist: xarray-spatial
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build; extra == 'dev'
|
|
33
|
+
Requires-Dist: isort; extra == 'dev'
|
|
34
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
35
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
39
|
+
Requires-Dist: tox; extra == 'dev'
|
|
40
|
+
Requires-Dist: twine; extra == 'dev'
|
|
41
|
+
Provides-Extra: doc
|
|
42
|
+
Requires-Dist: click==8.2.1; extra == 'doc'
|
|
43
|
+
Requires-Dist: mkdocs; extra == 'doc'
|
|
44
|
+
Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == 'doc'
|
|
45
|
+
Requires-Dist: mkdocs-material; extra == 'doc'
|
|
46
|
+
Requires-Dist: mkdocstrings; extra == 'doc'
|
|
47
|
+
Requires-Dist: mkdocstrings-python; extra == 'doc'
|
|
48
|
+
Requires-Dist: pymdown-extensions; extra == 'doc'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# BERA Tools
|
|
52
|
+
|
|
53
|
+
BERA Tools is successor of [Forest Line Mapper](https://github.com/appliedgrg/flm). It is a toolset for enhanced delineation and attribution of linear disturbances in forests.
|
|
54
|
+
|
|
55
|
+
<div align="center">
|
|
56
|
+
|
|
57
|
+
[](https://github.com/appliedgrg/beratools/actions/workflows/python-tests.yml)
|
|
58
|
+
[](https://codecov.io/gh/appliedgrg/beratools)
|
|
59
|
+
[](https://appliedgrg.github.io/beratools/)
|
|
60
|
+
[](https://anaconda.org/AppliedGRG/beratools)
|
|
61
|
+
[](https://www.python.org/downloads/release/python-3100/)
|
|
62
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
63
|
+
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
## [Quick Start](https://appliedgrg.github.io/beratools)
|
|
67
|
+
|
|
68
|
+
BERA Tools is built upon open-source Python libraries. Anaconda is used to manage runtime environments.
|
|
69
|
+
|
|
70
|
+
Ways to install BERA Tools:
|
|
71
|
+
|
|
72
|
+
- Windows installer
|
|
73
|
+
- Install with Anaconda.
|
|
74
|
+
|
|
75
|
+
### Windows Installer
|
|
76
|
+
|
|
77
|
+
Windows installer is provided with releases. Check the [latest release](https://github.com/appliedgrg/beratools/releases/latest) for the up-to-date installer.
|
|
78
|
+
|
|
79
|
+
### Install with Anaconda
|
|
80
|
+
|
|
81
|
+
Install with Anaconda works on Windows, macOS, and Linux.
|
|
82
|
+
|
|
83
|
+
- Install Miniconda. Download Miniconda from [Miniconda](https://docs.anaconda.com/miniconda/) and install on your machine.
|
|
84
|
+
- Download the file [environment.yml](https://raw.githubusercontent.com/appliedgrg/beratools/main/environment.yml
|
|
85
|
+
) and save to local storage. Launch **Anaconda Prompt** or **Miniconda Prompt**.
|
|
86
|
+
- **Change directory** to where environment.yml is saved in the command prompt.
|
|
87
|
+
- Run the following command to create a new environment named **bera**. **BERA Tools** will be installed in the new environment at the same time.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
$ conda env create -n bera -f environment.yml
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Wait until the installation is done.
|
|
94
|
+
- Activate the **bera** environment and launch BERA Tools:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
$ conda activate bera
|
|
98
|
+
$ beratools
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- [Download latest example data](https://github.com/appliedgrg/beratools/releases/latest/download/test_data.zip) to try with BERA Tools.
|
|
102
|
+
- To update BERA Tools when new release is issued, run the following commands:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
$ conda activate bera
|
|
106
|
+
$ conda update beratools
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- To completely remove BERA Tools and its environment, run the following command:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
$ conda remove -n bera
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## BERA Tools Guide
|
|
116
|
+
|
|
117
|
+
Check the online [BERA Tools Guide](https://appliedgrg.github.io/beratools/) for user, developer guides.
|
|
118
|
+
|
|
119
|
+
## Credits
|
|
120
|
+
|
|
121
|
+
<table>
|
|
122
|
+
<tr>
|
|
123
|
+
<td><img src="docs/files/icons/bera_logo.png" alt="Logos" width="80"></td>
|
|
124
|
+
<td>
|
|
125
|
+
<p>
|
|
126
|
+
This tool is part of the <strong><a href="http://www.beraproject.org/">Boreal Ecosystem Recovery & Assessment (BERA)</a></strong>.
|
|
127
|
+
It is actively developed by the <a href="https://www.appliedgrg.ca/"><strong>Applied Geospatial Research Group</strong></a>.
|
|
128
|
+
</p>
|
|
129
|
+
<p>
|
|
130
|
+
© 2026 Applied Geospatial Research Group. All rights reserved.
|
|
131
|
+
</p>
|
|
132
|
+
</td>
|
|
133
|
+
</tr>
|
|
134
|
+
</table>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
beratools/__init__.py,sha256=PW8O7ptzcibf-RBahr8etZ5bSyEmBNsj5HhR6zIQ64A,266
|
|
2
|
+
beratools/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
beratools/core/algo_canopy_footprint_exp.py,sha256=IlfwyMo7ySwx_UG9H91aEnHaLYkBoyeHg_4bj8d_IHw,22603
|
|
4
|
+
beratools/core/algo_centerline.py,sha256=HvXViuMwoOt9Xt3i8KX4bvpsZNz4zKv7LvdGIVvbIdA,15627
|
|
5
|
+
beratools/core/algo_common.py,sha256=vMGcjyF_pP79bqjNvjuBN_i9HoebUq2JWyHNyyKOObE,14714
|
|
6
|
+
beratools/core/algo_cost.py,sha256=JFNdy9U4B2-AgUR_o-zKABEINqU8GJLFJIAxFOm05Zw,5934
|
|
7
|
+
beratools/core/algo_dijkstra.py,sha256=_pABOkwTm3Tc9J33G_1or_iW96D4UPuL93iPlg_vK6k,15167
|
|
8
|
+
beratools/core/algo_line_grouping.py,sha256=_VuxMntZE3rAwJfZZwOkvFSUwyhxpi1nFapJ4ZOtT8Q,32998
|
|
9
|
+
beratools/core/algo_merge_lines.py,sha256=EKgGRyuomc7JpliK1nwCrOA8uwepGatmPaicxl0TTH0,7950
|
|
10
|
+
beratools/core/algo_split_with_lines.py,sha256=-k8oMBQ-XM3Xzdxdd9Vhi-YgMaRFHixdW8Ts9lL6dYU,10747
|
|
11
|
+
beratools/core/algo_vertex_optimization.py,sha256=OvvDOt7pi8X0xpaR45JH9e6WQVL-2ycWfBG_R0jMnHc,15380
|
|
12
|
+
beratools/core/canopy_threshold_relative.py,sha256=N5HIJiA6SgLA8qrML6qAbkJlxs3sEEKGnlvkfZ3mdbg,30015
|
|
13
|
+
beratools/core/constants.py,sha256=h4rnXNbKd7HCkyJNIrh01OKBegQb-tQrwavzrbCEUTM,1115
|
|
14
|
+
beratools/core/line_footprint_functions.py,sha256=B5PExYq4xZMMGvvnIwi3vDxhoG25NEwerthI3uOdCYU,31864
|
|
15
|
+
beratools/core/logger.py,sha256=55nhQV3Jjjf3GD-v6hfd-LKc8YyH4Wpe53ImQhLGwfM,2803
|
|
16
|
+
beratools/core/tool_base.py,sha256=dDJPUPf2qmjFRrbkqFR49r0osEEKmTLNrPQ3PbBtj5E,4343
|
|
17
|
+
beratools/gui/bt_data.py,sha256=VebUxYqRVJf68sMcRJLRWovj71ZwE8608RG0gVMYCbA,31024
|
|
18
|
+
beratools/gui/bt_gui_main.py,sha256=X-UGSl90a21OCINzqDysxVm5gWdlgkXejPal8ocoGIE,26877
|
|
19
|
+
beratools/gui/main.py,sha256=6rireFhO1NNuuKlyU89HT7ESq3_zrex5xlloGYQO6WM,482
|
|
20
|
+
beratools/gui/tool_widgets.py,sha256=mCZrudV33uamPxb2JMp1ZU2e8gNBXh5dq85ur8MBm4s,35876
|
|
21
|
+
beratools/gui/assets/BERALogo.ico,sha256=5kNFkKe0FdL-oBXiO2ExVTMHhelO6sB69I3VHPCRQno,107799
|
|
22
|
+
beratools/gui/assets/BERALogo.png,sha256=hnGLSL7Jv2ljDn6Z8ad5ekzH9k1PEuq7xe7Hh8bXZkE,6660
|
|
23
|
+
beratools/gui/assets/BERA_Splash.gif,sha256=4QB5Xua2F59mBeOoeQ1Eoz0t-fZOQbYMTAl62RE2yEU,4377179
|
|
24
|
+
beratools/gui/assets/BERA_WizardImage.png,sha256=IKgbY0h_uZ3Sk2B7-X5oMIs5GkNwLXgbf87JjuealvY,11370
|
|
25
|
+
beratools/gui/assets/beratools.json,sha256=KJzkd5DyXaou_MfOg_glEc7xOfDB6cGItRzOzK9V9VY,18964
|
|
26
|
+
beratools/gui/assets/closed.gif,sha256=T9BBGl286-FdSRYbWVt68HwPRF0geWoowtBzeYsk_bI,592
|
|
27
|
+
beratools/gui/assets/closed.png,sha256=FRSAh3wzASowFUEt88zHhk1BQmwRbjoCs50Ff8KpJNQ,311
|
|
28
|
+
beratools/gui/assets/gui.json,sha256=kRD3Ozim9IotcY8P3fapgs4qqj5vebQHLxmss_SOJdY,329
|
|
29
|
+
beratools/gui/assets/open.gif,sha256=o8LL1OaDNkQ0Gcc9dO-1f1UZFAQ3ALc4svtqiUuWJPY,1062
|
|
30
|
+
beratools/gui/assets/open.png,sha256=v0OOwfTNGMmtdFbffCbxdYLaNZeRC9Cy5whAwi71oVw,524
|
|
31
|
+
beratools/gui/assets/tool.gif,sha256=2KGcUI_b-qFdrJJhgbx9pMZ8pExX7zH8nYvweRiQs6U,555
|
|
32
|
+
beratools/gui/assets/tool.png,sha256=35Txwum2l_aKeazF_I97ZaZAKHsrKQCGG-LZxEIFyZw,714
|
|
33
|
+
beratools/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
beratools/tools/canopy_footprint_absolute.py,sha256=gcQjCDc21vcatcF5IQVUmwVQSUn9drHn09yvnnfYsqI,7865
|
|
35
|
+
beratools/tools/canopy_footprint_exp.py,sha256=91Nh0MCS6J0sTx65tHeR122uXzwEvT1rtaisq8yB1qQ,4185
|
|
36
|
+
beratools/tools/centerline.py,sha256=Zn0iPEL0fki6VMENCDqaPIrM433a-b_9PGjyxEfx3S4,3956
|
|
37
|
+
beratools/tools/check_seed_line.py,sha256=653X2R75zP667VWPylwu5pLjLfiSCI-z6va_NCXa55E,4389
|
|
38
|
+
beratools/tools/common.py,sha256=b_4b3RuBSiQVmtwWSDpXEZvROnBY0vIQzpDT5TW_nfw,11183
|
|
39
|
+
beratools/tools/ground_footprint.py,sha256=VhVdB6ZPBG4zxZCHUWSZnpIn1aq_uB5RebvKVi-ZaRA,16177
|
|
40
|
+
beratools/tools/line_footprint_relative.py,sha256=DJQiS4aQRN-KjOENl04qPDxBQfza4iicS39Xjd7-Zdw,3716
|
|
41
|
+
beratools/tools/tool_template.py,sha256=U4YYGWCXLGQImE96tjR6tx3eUSTTAs2TuGmMbF6zlME,2661
|
|
42
|
+
beratools/tools/vertex_optimization.py,sha256=b8PU5VHFx8Z42lgTl4P7AB5DaoJ8USztHGoU9s-iBW0,1591
|
|
43
|
+
beratools/utility/env_checks.py,sha256=-NBUJc1oh4XzcjNvY-IgvTLs7_zcHS3_KdRjXdYz790,1606
|
|
44
|
+
beratools/utility/spatial_common.py,sha256=GA2FwbxKeqJPfX7jtrnE6LuH9PgBlMujgG-ejoXlOrQ,6732
|
|
45
|
+
beratools/utility/tool_args.py,sha256=If8qGoK-snnTZuWhtPA_xSOVY5DMZPlZ-CGoSuy2-tk,5251
|
|
46
|
+
beratools-0.2.5.dist-info/METADATA,sha256=dYF7hTCCmeryiJX-Y0Nnh4ZcP88bJ02m82E2NJALcuQ,5317
|
|
47
|
+
beratools-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
beratools-0.2.5.dist-info/entry_points.txt,sha256=wEzv7VVMxnmCmzV3D10B-9DXdqnZgPsBgVWtiIhcdaU,98
|
|
49
|
+
beratools-0.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
50
|
+
beratools-0.2.5.dist-info/RECORD,,
|