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.
- beratools/__init__.py +1 -7
- beratools/core/algo_centerline.py +491 -351
- beratools/core/algo_common.py +497 -0
- beratools/core/algo_cost.py +192 -0
- beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
- beratools/core/algo_footprint_rel.py +577 -0
- beratools/core/algo_line_grouping.py +944 -0
- beratools/core/algo_merge_lines.py +214 -0
- beratools/core/algo_split_with_lines.py +304 -0
- beratools/core/algo_tiler.py +428 -0
- beratools/core/algo_vertex_optimization.py +469 -0
- beratools/core/constants.py +52 -86
- beratools/core/logger.py +76 -85
- beratools/core/tool_base.py +196 -133
- beratools/gui/__init__.py +11 -15
- beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
- beratools/gui/batch_processing_dlg.py +513 -463
- beratools/gui/bt_data.py +481 -487
- beratools/gui/bt_gui_main.py +710 -691
- beratools/gui/main.py +26 -0
- beratools/gui/map_window.py +162 -146
- beratools/gui/tool_widgets.py +725 -493
- beratools/tools/Beratools_r_script.r +1120 -1120
- beratools/tools/Ht_metrics.py +116 -116
- beratools/tools/__init__.py +7 -7
- beratools/tools/batch_processing.py +136 -132
- beratools/tools/canopy_threshold_relative.py +672 -670
- beratools/tools/canopycostraster.py +222 -222
- beratools/tools/centerline.py +136 -176
- beratools/tools/common.py +857 -885
- beratools/tools/fl_regen_csf.py +428 -428
- beratools/tools/forest_line_attributes.py +408 -408
- beratools/tools/line_footprint_absolute.py +213 -363
- beratools/tools/line_footprint_fixed.py +436 -282
- beratools/tools/line_footprint_functions.py +733 -720
- beratools/tools/line_footprint_relative.py +73 -64
- beratools/tools/line_grouping.py +45 -0
- beratools/tools/ln_relative_metrics.py +615 -615
- beratools/tools/r_cal_lpi_elai.r +24 -24
- beratools/tools/r_generate_pd_focalraster.r +100 -100
- beratools/tools/r_interface.py +79 -79
- beratools/tools/r_point_density.r +8 -8
- beratools/tools/rpy_chm2trees.py +86 -86
- beratools/tools/rpy_dsm_chm_by.py +81 -81
- beratools/tools/rpy_dtm_by.py +63 -63
- beratools/tools/rpy_find_cellsize.py +43 -43
- beratools/tools/rpy_gnd_csf.py +74 -74
- beratools/tools/rpy_hummock_hollow.py +85 -85
- beratools/tools/rpy_hummock_hollow_raster.py +71 -71
- beratools/tools/rpy_las_info.py +51 -51
- beratools/tools/rpy_laz2las.py +40 -40
- beratools/tools/rpy_lpi_elai_lascat.py +466 -466
- beratools/tools/rpy_normalized_lidar_by.py +56 -56
- beratools/tools/rpy_percent_above_dbh.py +80 -80
- beratools/tools/rpy_points2trees.py +88 -88
- beratools/tools/rpy_vegcoverage.py +94 -94
- beratools/tools/tiler.py +48 -206
- beratools/tools/tool_template.py +69 -54
- beratools/tools/vertex_optimization.py +61 -620
- beratools/tools/zonal_threshold.py +144 -144
- beratools-0.2.1.dist-info/METADATA +109 -0
- beratools-0.2.1.dist-info/RECORD +74 -0
- {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/WHEEL +1 -1
- {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/licenses/LICENSE +22 -22
- beratools/gui/cli.py +0 -18
- beratools/gui/gui.json +0 -8
- beratools/gui_tk/ASCII Banners.txt +0 -248
- beratools/gui_tk/__init__.py +0 -20
- beratools/gui_tk/beratools_main.py +0 -515
- beratools/gui_tk/bt_widgets.py +0 -442
- beratools/gui_tk/cli.py +0 -18
- beratools/gui_tk/img/BERALogo.png +0 -0
- beratools/gui_tk/img/closed.gif +0 -0
- beratools/gui_tk/img/closed.png +0 -0
- beratools/gui_tk/img/open.gif +0 -0
- beratools/gui_tk/img/open.png +0 -0
- beratools/gui_tk/img/tool.gif +0 -0
- beratools/gui_tk/img/tool.png +0 -0
- beratools/gui_tk/main.py +0 -14
- beratools/gui_tk/map_window.py +0 -144
- beratools/gui_tk/runner.py +0 -1481
- beratools/gui_tk/tooltip.py +0 -55
- beratools/third_party/pyqtlet2/__init__.py +0 -9
- beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
- beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
- beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
- beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
- beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
- beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
- beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
- beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
- beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
- beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
- beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
- beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
- beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
- beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
- beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
- beratools/third_party/pyqtlet2/mapwidget.py +0 -45
- beratools/third_party/pyqtlet2/web/custom.js +0 -43
- beratools/third_party/pyqtlet2/web/map.html +0 -23
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
- beratools/tools/forest_line_ecosite.py +0 -216
- beratools/tools/lapis_all.py +0 -103
- beratools/tools/least_cost_path_from_chm.py +0 -152
- beratools-0.2.0.dist-info/METADATA +0 -63
- beratools-0.2.0.dist-info/RECORD +0 -142
- /beratools/gui/{img → assets}/BERALogo.png +0 -0
- /beratools/gui/{img → assets}/closed.gif +0 -0
- /beratools/gui/{img → assets}/closed.png +0 -0
- /beratools/{gui_tk → gui/assets}/gui.json +0 -0
- /beratools/gui/{img → assets}/open.gif +0 -0
- /beratools/gui/{img → assets}/open.png +0 -0
- /beratools/gui/{img → assets}/tool.gif +0 -0
- /beratools/gui/{img → assets}/tool.png +0 -0
- {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,469 @@
|
|
|
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 move line vertices to the right
|
|
14
|
+
seismic line courses for improved alignment and analysis in
|
|
15
|
+
geospatial data processing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import geopandas as gpd
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import shapely.geometry as sh_geom
|
|
24
|
+
from shapely import STRtree
|
|
25
|
+
|
|
26
|
+
import beratools.core.algo_common as algo_common
|
|
27
|
+
import beratools.core.algo_cost as algo_cost
|
|
28
|
+
import beratools.core.constants as bt_const
|
|
29
|
+
import beratools.core.tool_base as bt_base
|
|
30
|
+
import beratools.tools.common as bt_common
|
|
31
|
+
from beratools.core import algo_dijkstra
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def update_line_end_pt(line, index, new_vertex):
|
|
35
|
+
if not line:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if index >= len(line.coords) or index < -1:
|
|
39
|
+
return line
|
|
40
|
+
|
|
41
|
+
coords = list(line.coords)
|
|
42
|
+
if len(coords[index]) == 2:
|
|
43
|
+
coords[index] = (new_vertex.x, new_vertex.y)
|
|
44
|
+
elif len(coords[index]) == 3:
|
|
45
|
+
coords[index] = (new_vertex.x, new_vertex.y, 0.0)
|
|
46
|
+
|
|
47
|
+
return sh_geom.LineString(coords)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _SingleLine:
|
|
51
|
+
"""Single line object with anchor point."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, line_gdf, line_no, end_no, search_distance):
|
|
54
|
+
self.line_gdf = line_gdf
|
|
55
|
+
self.line = self.line_gdf.geometry[0]
|
|
56
|
+
self.line_no = line_no
|
|
57
|
+
self.end_no = end_no
|
|
58
|
+
self.search_distance = search_distance
|
|
59
|
+
self.anchor = None
|
|
60
|
+
|
|
61
|
+
self.add_anchors_to_line()
|
|
62
|
+
|
|
63
|
+
def is_valid(self):
|
|
64
|
+
return self.line.is_valid
|
|
65
|
+
|
|
66
|
+
def line_coord_list(self):
|
|
67
|
+
return algo_common.line_coord_list(self.line)
|
|
68
|
+
|
|
69
|
+
def get_end_vertex(self):
|
|
70
|
+
return self.line_coord_list()[self.end_no]
|
|
71
|
+
|
|
72
|
+
def touches_point(self, vertex):
|
|
73
|
+
return algo_common.points_are_close(vertex, self.get_end_vertex())
|
|
74
|
+
|
|
75
|
+
def get_angle(self):
|
|
76
|
+
return algo_common.get_angle(self.line, self.end_no)
|
|
77
|
+
|
|
78
|
+
def add_anchors_to_line(self):
|
|
79
|
+
"""
|
|
80
|
+
Append new vertex to vertex group, by calculating distance to existing vertices.
|
|
81
|
+
|
|
82
|
+
An anchor point will be added together with line
|
|
83
|
+
"""
|
|
84
|
+
# Calculate anchor point for each vertex
|
|
85
|
+
point = self.get_end_vertex()
|
|
86
|
+
line_string = self.line
|
|
87
|
+
index = self.end_no
|
|
88
|
+
pts = algo_common.line_coord_list(line_string)
|
|
89
|
+
|
|
90
|
+
pt_1 = None
|
|
91
|
+
pt_2 = None
|
|
92
|
+
if index == 0:
|
|
93
|
+
pt_1 = point
|
|
94
|
+
pt_2 = pts[1]
|
|
95
|
+
elif index == -1:
|
|
96
|
+
pt_1 = point
|
|
97
|
+
pt_2 = pts[-2]
|
|
98
|
+
|
|
99
|
+
# Calculate anchor point
|
|
100
|
+
dist_pt = 0.0
|
|
101
|
+
if pt_1 and pt_2:
|
|
102
|
+
dist_pt = pt_1.distance(pt_2)
|
|
103
|
+
|
|
104
|
+
# TODO: check why two points are the same
|
|
105
|
+
if np.isclose(dist_pt, 0.0):
|
|
106
|
+
print("Points are close, return")
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
X = pt_1.x + (pt_2.x - pt_1.x) * self.search_distance / dist_pt
|
|
110
|
+
Y = pt_1.y + (pt_2.y - pt_1.y) * self.search_distance / dist_pt
|
|
111
|
+
self.anchor = sh_geom.Point(X, Y) # add anchor point
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _Vertex:
|
|
115
|
+
"""Vertex object with multiple lines."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, line_obj):
|
|
118
|
+
self.vertex = line_obj.get_end_vertex()
|
|
119
|
+
self.search_distance = line_obj.search_distance
|
|
120
|
+
|
|
121
|
+
self.cost_footprint = None
|
|
122
|
+
self.vertex_opt = None # optimized vertex
|
|
123
|
+
self.centerlines = None
|
|
124
|
+
self.anchors = None
|
|
125
|
+
self.in_raster = None
|
|
126
|
+
self.line_radius = None
|
|
127
|
+
self.lines = [] # SingleLine objects
|
|
128
|
+
|
|
129
|
+
self.add_line(line_obj)
|
|
130
|
+
|
|
131
|
+
def add_line(self, line_obj):
|
|
132
|
+
self.lines.append(line_obj)
|
|
133
|
+
|
|
134
|
+
def generate_anchor_pairs(self):
|
|
135
|
+
"""
|
|
136
|
+
Extend line following outward direction to length of search_distance.
|
|
137
|
+
|
|
138
|
+
Use the end point as anchor point.
|
|
139
|
+
|
|
140
|
+
vertex: input intersection with all related lines
|
|
141
|
+
return:
|
|
142
|
+
one or two pairs of anchors according to numbers of lines
|
|
143
|
+
intersected.
|
|
144
|
+
two pairs anchors return when 3 or 4 lines intersected
|
|
145
|
+
one pair anchors return when 1 or 2 lines intersected.
|
|
146
|
+
"""
|
|
147
|
+
lines = self.get_lines()
|
|
148
|
+
vertex = self.get_vertex()
|
|
149
|
+
slopes = []
|
|
150
|
+
for line in self.lines:
|
|
151
|
+
slopes.append(line.get_angle())
|
|
152
|
+
|
|
153
|
+
index = 0 # the index of line which paired with first line.
|
|
154
|
+
pt_start_1 = None
|
|
155
|
+
pt_end_1 = None
|
|
156
|
+
pt_start_2 = None
|
|
157
|
+
pt_end_2 = None
|
|
158
|
+
|
|
159
|
+
if len(slopes) == 4:
|
|
160
|
+
# get sort order of angles
|
|
161
|
+
index = np.argsort(slopes)
|
|
162
|
+
|
|
163
|
+
# first anchor pair (first and third in the sorted array)
|
|
164
|
+
pt_start_1 = self.lines[index[0]].anchor
|
|
165
|
+
pt_end_1 = self.lines[index[2]].anchor
|
|
166
|
+
|
|
167
|
+
pt_start_2 = self.lines[index[1]].anchor
|
|
168
|
+
pt_end_2 = self.lines[index[3]].anchor
|
|
169
|
+
elif len(slopes) == 3:
|
|
170
|
+
# find the largest difference between angles
|
|
171
|
+
angle_diff = [
|
|
172
|
+
abs(slopes[0] - slopes[1]),
|
|
173
|
+
abs(slopes[0] - slopes[2]),
|
|
174
|
+
abs(slopes[1] - slopes[2]),
|
|
175
|
+
]
|
|
176
|
+
angle_diff_norm = [2 * np.pi - i if i > np.pi else i for i in angle_diff]
|
|
177
|
+
index = np.argmax(angle_diff_norm)
|
|
178
|
+
pairs = [(0, 1), (0, 2), (1, 2)]
|
|
179
|
+
pair = pairs[index]
|
|
180
|
+
|
|
181
|
+
# first anchor pair
|
|
182
|
+
pt_start_1 = self.lines[pair[0]].anchor
|
|
183
|
+
pt_end_1 = self.lines[pair[1]].anchor
|
|
184
|
+
|
|
185
|
+
# the rest one index
|
|
186
|
+
remain = list({0, 1, 2} - set(pair))[0] # the remaining index
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
pt_start_2 = lines[remain][2]
|
|
190
|
+
# symmetry point of pt_start_2 regarding vertex["point"]
|
|
191
|
+
X = vertex.x - (pt_start_2.x - vertex.x)
|
|
192
|
+
Y = vertex.y - (pt_start_2.y - vertex.y)
|
|
193
|
+
pt_end_2 = sh_geom.Point(X, Y)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(e)
|
|
196
|
+
|
|
197
|
+
# this scenario only use two anchors
|
|
198
|
+
# and find the closest point on least cost path
|
|
199
|
+
elif len(slopes) == 2:
|
|
200
|
+
pt_start_1 = self.lines[0].anchor
|
|
201
|
+
pt_end_1 = self.lines[1].anchor
|
|
202
|
+
elif len(slopes) == 1:
|
|
203
|
+
pt_start_1 = self.lines[0].anchor
|
|
204
|
+
# symmetry point of pt_start_1 regarding vertex["point"]
|
|
205
|
+
X = vertex.x - (pt_start_1.x - vertex.x)
|
|
206
|
+
Y = vertex.y - (pt_start_1.y - vertex.y)
|
|
207
|
+
pt_end_1 = sh_geom.Point(X, Y)
|
|
208
|
+
|
|
209
|
+
if not pt_start_1 or not pt_end_1:
|
|
210
|
+
print("Anchors not found")
|
|
211
|
+
|
|
212
|
+
# if points are outside of cost footprint, set to None
|
|
213
|
+
points = [pt_start_1, pt_end_1, pt_start_2, pt_end_2]
|
|
214
|
+
for index, pt in enumerate(points):
|
|
215
|
+
if pt:
|
|
216
|
+
if not self.cost_footprint.contains(sh_geom.Point(pt)):
|
|
217
|
+
points[index] = None
|
|
218
|
+
|
|
219
|
+
if len(slopes) == 4 or len(slopes) == 3:
|
|
220
|
+
if None in points:
|
|
221
|
+
return None
|
|
222
|
+
else:
|
|
223
|
+
return points
|
|
224
|
+
elif len(slopes) == 2 or len(slopes) == 1:
|
|
225
|
+
if None in (pt_start_1, pt_end_1):
|
|
226
|
+
return None
|
|
227
|
+
else:
|
|
228
|
+
return pt_start_1, pt_end_1
|
|
229
|
+
|
|
230
|
+
def compute(self):
|
|
231
|
+
try:
|
|
232
|
+
self.anchors = self.generate_anchor_pairs()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(e)
|
|
235
|
+
|
|
236
|
+
if not self.anchors:
|
|
237
|
+
if bt_const.BT_DEBUGGING:
|
|
238
|
+
print("No anchors retrieved")
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
centerline_1 = None
|
|
242
|
+
centerline_2 = None
|
|
243
|
+
intersection = None
|
|
244
|
+
|
|
245
|
+
if bt_const.CenterlineFlags.USE_SKIMAGE_GRAPH:
|
|
246
|
+
find_lc_path = algo_dijkstra.find_least_cost_path_skimage
|
|
247
|
+
else:
|
|
248
|
+
find_lc_path = algo_dijkstra.find_least_cost_path
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
if len(self.anchors) == 4:
|
|
252
|
+
seed_line = sh_geom.LineString(self.anchors[0:2])
|
|
253
|
+
|
|
254
|
+
raster_clip, out_meta = bt_common.clip_raster(
|
|
255
|
+
self.in_raster, seed_line, self.line_radius
|
|
256
|
+
)
|
|
257
|
+
raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
|
|
258
|
+
centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
|
|
259
|
+
seed_line = sh_geom.LineString(self.anchors[2:4])
|
|
260
|
+
|
|
261
|
+
raster_clip, out_meta = bt_common.clip_raster(
|
|
262
|
+
self.in_raster, seed_line, self.line_radius
|
|
263
|
+
)
|
|
264
|
+
raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
|
|
265
|
+
centerline_2 = find_lc_path(raster_clip, out_meta, seed_line)
|
|
266
|
+
|
|
267
|
+
if centerline_1 and centerline_2:
|
|
268
|
+
intersection = algo_common.intersection_of_lines(
|
|
269
|
+
centerline_1, centerline_2
|
|
270
|
+
)
|
|
271
|
+
elif len(self.anchors) == 2:
|
|
272
|
+
seed_line = sh_geom.LineString(self.anchors)
|
|
273
|
+
|
|
274
|
+
raster_clip, out_meta = bt_common.clip_raster(
|
|
275
|
+
self.in_raster, seed_line, self.line_radius
|
|
276
|
+
)
|
|
277
|
+
raster_clip, _ = algo_cost.cost_raster(raster_clip, out_meta)
|
|
278
|
+
centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
|
|
279
|
+
|
|
280
|
+
if centerline_1:
|
|
281
|
+
intersection = algo_common.closest_point_to_line(
|
|
282
|
+
self.get_vertex(), centerline_1
|
|
283
|
+
)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
print(e)
|
|
286
|
+
|
|
287
|
+
# Update vertices according to intersection, new center lines are returned
|
|
288
|
+
if type(intersection) is sh_geom.MultiPoint:
|
|
289
|
+
intersection = intersection.centroid
|
|
290
|
+
|
|
291
|
+
self.centerlines = [centerline_1, centerline_2]
|
|
292
|
+
self.vertex_opt = intersection
|
|
293
|
+
|
|
294
|
+
def get_lines(self):
|
|
295
|
+
lines = [item.line for item in self.lines]
|
|
296
|
+
return lines
|
|
297
|
+
|
|
298
|
+
def get_vertex(self):
|
|
299
|
+
return self.vertex
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class VertexGrouping:
|
|
303
|
+
"""A class used to group vertices and perform vertex optimization."""
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
in_line,
|
|
308
|
+
in_raster,
|
|
309
|
+
search_distance,
|
|
310
|
+
line_radius,
|
|
311
|
+
out_line,
|
|
312
|
+
processes,
|
|
313
|
+
verbose,
|
|
314
|
+
in_layer=None,
|
|
315
|
+
out_layer=None,
|
|
316
|
+
):
|
|
317
|
+
self.in_line = in_line
|
|
318
|
+
self.in_raster = in_raster
|
|
319
|
+
self.line_radius = float(line_radius)
|
|
320
|
+
self.search_distance = float(search_distance)
|
|
321
|
+
self.out_line = out_line
|
|
322
|
+
self.processes = processes
|
|
323
|
+
self.verbose = verbose
|
|
324
|
+
self.parallel_mode = bt_const.PARALLEL_MODE
|
|
325
|
+
self.in_layer = in_layer
|
|
326
|
+
self.out_layer = out_layer
|
|
327
|
+
|
|
328
|
+
self.crs = None
|
|
329
|
+
self.vertex_grp = []
|
|
330
|
+
self.sindex = None
|
|
331
|
+
|
|
332
|
+
self.line_list = []
|
|
333
|
+
self.line_visited = None
|
|
334
|
+
|
|
335
|
+
# calculate cost raster footprint
|
|
336
|
+
self.cost_footprint = algo_common.generate_raster_footprint(
|
|
337
|
+
self.in_raster, latlon=False
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def set_parallel_mode(self, parallel_mode):
|
|
341
|
+
self.parallel_mode = parallel_mode
|
|
342
|
+
|
|
343
|
+
def create_vertex_group(self, line_obj):
|
|
344
|
+
"""
|
|
345
|
+
Create a new vertex group.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
line_obj : _SingleLine
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
# all end points not added will stay with this vertex
|
|
352
|
+
vertex = line_obj.get_end_vertex()
|
|
353
|
+
vertex_obj = _Vertex(line_obj)
|
|
354
|
+
search = self.sindex.query(vertex.buffer(bt_const.SMALL_BUFFER))
|
|
355
|
+
|
|
356
|
+
# add more vertices to the new group
|
|
357
|
+
for i in search:
|
|
358
|
+
line = self.line_list[i]
|
|
359
|
+
if i == line_obj.line_no:
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
if not self.line_visited[i][0]:
|
|
363
|
+
new_line = _SingleLine(line, i, 0, self.search_distance)
|
|
364
|
+
if new_line.touches_point(vertex):
|
|
365
|
+
vertex_obj.add_line(new_line)
|
|
366
|
+
self.line_visited[i][0] = True
|
|
367
|
+
|
|
368
|
+
if not self.line_visited[i][-1]:
|
|
369
|
+
new_line = _SingleLine(line, i, -1, self.search_distance)
|
|
370
|
+
if new_line.touches_point(vertex):
|
|
371
|
+
vertex_obj.add_line(new_line)
|
|
372
|
+
self.line_visited[i][-1] = True
|
|
373
|
+
|
|
374
|
+
vertex_obj.in_raster = self.in_raster
|
|
375
|
+
|
|
376
|
+
vertex_obj.line_radius = self.line_radius
|
|
377
|
+
vertex_obj.cost_footprint = self.cost_footprint
|
|
378
|
+
self.vertex_grp.append(vertex_obj)
|
|
379
|
+
|
|
380
|
+
def create_all_vertex_groups(self):
|
|
381
|
+
self.line_list = algo_common.prepare_lines_gdf(
|
|
382
|
+
self.in_line, layer=self.in_layer, proc_segments=True
|
|
383
|
+
)
|
|
384
|
+
self.sindex = STRtree([item.geometry[0] for item in self.line_list])
|
|
385
|
+
self.line_visited = [{0: False, -1: False} for _ in range(len(self.line_list))]
|
|
386
|
+
|
|
387
|
+
i = 0
|
|
388
|
+
for line_no in range(len(self.line_list)):
|
|
389
|
+
if not self.line_visited[line_no][0]:
|
|
390
|
+
line = _SingleLine(
|
|
391
|
+
self.line_list[line_no], line_no, 0, self.search_distance
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if not line.is_valid:
|
|
395
|
+
print(f"Line {line['line_no']} is invalid")
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
self.create_vertex_group(line)
|
|
399
|
+
self.line_visited[line_no][0] = True
|
|
400
|
+
i += 1
|
|
401
|
+
|
|
402
|
+
if not self.line_visited[line_no][-1]:
|
|
403
|
+
line = _SingleLine(
|
|
404
|
+
self.line_list[line_no], line_no, -1, self.search_distance
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if not line.is_valid:
|
|
408
|
+
print(f"Line {line['line_no']} is invalid")
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
self.create_vertex_group(line)
|
|
412
|
+
self.line_visited[line_no][-1] = True
|
|
413
|
+
i += 1
|
|
414
|
+
|
|
415
|
+
def update_all_lines(self):
|
|
416
|
+
for vertex_obj in self.vertex_grp:
|
|
417
|
+
for line in vertex_obj.lines:
|
|
418
|
+
if not vertex_obj.vertex_opt:
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
old_line = self.line_list[line.line_no].geometry[0]
|
|
422
|
+
self.line_list[line.line_no].geometry = [
|
|
423
|
+
update_line_end_pt(old_line, line.end_no, vertex_obj.vertex_opt)
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
def save_all_layers(self, line_file):
|
|
427
|
+
line_file = Path(line_file)
|
|
428
|
+
lines = pd.concat(self.line_list)
|
|
429
|
+
lines.to_file(line_file, layer=self.out_layer)
|
|
430
|
+
|
|
431
|
+
aux_file = line_file
|
|
432
|
+
if line_file.suffix == ".shp":
|
|
433
|
+
file_stem = line_file.stem
|
|
434
|
+
aux_file = line_file.with_stem(file_stem + "_aux").with_suffix(".gpkg")
|
|
435
|
+
|
|
436
|
+
lc_paths = []
|
|
437
|
+
anchors = []
|
|
438
|
+
vertices = []
|
|
439
|
+
for item in self.vertex_grp:
|
|
440
|
+
if item.centerlines:
|
|
441
|
+
lc_paths.extend(item.centerlines)
|
|
442
|
+
if item.anchors:
|
|
443
|
+
anchors.extend(item.anchors)
|
|
444
|
+
if item.vertex_opt:
|
|
445
|
+
vertices.append(item.vertex_opt)
|
|
446
|
+
|
|
447
|
+
lc_paths = [item for item in lc_paths if item is not None]
|
|
448
|
+
anchors = [item for item in anchors if item is not None]
|
|
449
|
+
vertices = [item for item in vertices if item is not None]
|
|
450
|
+
|
|
451
|
+
lc_paths = gpd.GeoDataFrame(geometry=lc_paths, crs=lines.crs)
|
|
452
|
+
anchors = gpd.GeoDataFrame(geometry=anchors, crs=lines.crs)
|
|
453
|
+
vertices = gpd.GeoDataFrame(geometry=vertices, crs=lines.crs)
|
|
454
|
+
|
|
455
|
+
lc_paths.to_file(aux_file, layer="lc_paths")
|
|
456
|
+
anchors.to_file(aux_file, layer="anchors")
|
|
457
|
+
vertices.to_file(aux_file, layer="vertices")
|
|
458
|
+
|
|
459
|
+
def compute(self):
|
|
460
|
+
vertex_grp = bt_base.execute_multiprocessing(
|
|
461
|
+
algo_common.process_single_item,
|
|
462
|
+
self.vertex_grp,
|
|
463
|
+
"Vertex Optimization",
|
|
464
|
+
self.processes,
|
|
465
|
+
1,
|
|
466
|
+
verbose=self.verbose,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
self.vertex_grp = vertex_grp
|
beratools/core/constants.py
CHANGED
|
@@ -1,86 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
SNAP_TOLERANCE = 15.0
|
|
54
|
-
SEGMENTIZE_LENGTH = 1.0
|
|
55
|
-
SIMPLIFY_LENGTH = 0.5
|
|
56
|
-
SMOOTH_SIGMA = 0.8
|
|
57
|
-
CLEANUP_POLYGON_BY_AREA = 1.0
|
|
58
|
-
POLYGON_BUFFER = 1e-6
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class CenterlineFlags(Flag):
|
|
62
|
-
USE_SKIMAGE_GRAPH = False
|
|
63
|
-
DELETE_HOLES = True
|
|
64
|
-
SIMPLIFY_POLYGON = True
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@unique
|
|
68
|
-
class CenterlineStatus(IntEnum):
|
|
69
|
-
SUCCESS = 1
|
|
70
|
-
FAILED = 2
|
|
71
|
-
REGENERATE_SUCCESS = 3
|
|
72
|
-
REGENERATE_FAILED = 4
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@unique
|
|
76
|
-
class ParallelMode(IntEnum):
|
|
77
|
-
SEQUENTIAL = 1
|
|
78
|
-
MULTIPROCESSING = 2
|
|
79
|
-
CONCURRENT = 3
|
|
80
|
-
DASK = 4
|
|
81
|
-
# RAY = 5
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
PARALLEL_MODE = ParallelMode.MULTIPROCESSING
|
|
85
|
-
|
|
86
|
-
|
|
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 common constants.
|
|
14
|
+
"""
|
|
15
|
+
import enum
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
NADDatum = ['NAD83 Canadian Spatial Reference System', 'North American Datum 1983']
|
|
20
|
+
|
|
21
|
+
ASSETS_PATH = "assets"
|
|
22
|
+
BT_DEBUGGING = False
|
|
23
|
+
BT_UID = 'BT_UID'
|
|
24
|
+
BT_GROUP = "group"
|
|
25
|
+
|
|
26
|
+
BT_EPSILON = np.finfo(float).eps
|
|
27
|
+
BT_NODATA_COST = np.inf
|
|
28
|
+
BT_NODATA = -9999
|
|
29
|
+
|
|
30
|
+
LP_SEGMENT_LENGTH = 500
|
|
31
|
+
FP_CORRIDOR_THRESHOLD = 2.5
|
|
32
|
+
SMALL_BUFFER = 1e-3
|
|
33
|
+
|
|
34
|
+
class CenterlineFlags(enum.Flag):
|
|
35
|
+
"""Flags for the centerline algorithm."""
|
|
36
|
+
|
|
37
|
+
USE_SKIMAGE_GRAPH = False
|
|
38
|
+
DELETE_HOLES = True
|
|
39
|
+
SIMPLIFY_POLYGON = True
|
|
40
|
+
|
|
41
|
+
@enum.unique
|
|
42
|
+
class ParallelMode(enum.IntEnum):
|
|
43
|
+
"""Defines the parallel mode for the algorithms."""
|
|
44
|
+
|
|
45
|
+
SEQUENTIAL = 1
|
|
46
|
+
MULTIPROCESSING = 2
|
|
47
|
+
CONCURRENT = 3
|
|
48
|
+
DASK = 4
|
|
49
|
+
SLURM = 5
|
|
50
|
+
# RAY = 6
|
|
51
|
+
|
|
52
|
+
PARALLEL_MODE = ParallelMode.MULTIPROCESSING
|