BERATools 0.2.0__py3-none-any.whl → 0.2.2__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.2.dist-info/METADATA +108 -0
  62. beratools-0.2.2.dist-info/RECORD +74 -0
  63. {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/WHEEL +1 -1
  64. {beratools-0.2.0.dist-info → beratools-0.2.2.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.2.dist-info}/entry_points.txt +0 -0
@@ -1,620 +1,61 @@
1
- #
2
- # Copyright (C) 2021 Applied Geospatial Research Group
3
- #
4
- # This program is free software: you can redistribute it and/or modify
5
- # it under the terms of the GNU General Public License as published by
6
- # the Free Software Foundation, version 3.
7
- #
8
- # This program is distributed in the hope that it will be useful,
9
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
- # GNU General Public License for more details.
12
- #
13
- # You should have received a copy of the GNU General Public License
14
- # along with this program. If not, see <https://gnu.org/licenses/gpl-3.0>.
15
- #
16
- # ---------------------------------------------------------------------------
17
- #
18
- # FLM_VertexOptimization.py
19
- # Script Author: Richard Zeng
20
- # Date: 2021-Oct-26
21
- #
22
- # This script is part of the Forest Line Mapper (FLM) toolset
23
- # Webpage: https://github.com/appliedgrg/flm
24
- #
25
- # Purpose: Move line vertices to right seismic line courses
26
- #
27
- # ---------------------------------------------------------------------------
28
- # System imports
29
- import sys
30
- import time
31
- from pathlib import Path
32
- from inspect import getsourcefile
33
-
34
- import fiona
35
- from shapely.geometry import shape, Point, MultiPoint, LineString, MultiLineString, GeometryCollection
36
- from shapely import STRtree
37
- from xrspatial import convolution
38
-
39
- from inspect import getsourcefile
40
-
41
-
42
- if __name__ == '__main__':
43
- current_file = Path(getsourcefile(lambda: 0)).resolve()
44
- btool_dir = current_file.parents[2]
45
- sys.path.insert(0, btool_dir.as_posix())
46
-
47
- from beratools.core.constants import *
48
- from beratools.core.tool_base import *
49
- from beratools.tools.common import *
50
- from beratools.core.dijkstra_algorithm import *
51
-
52
- DISTANCE_THRESHOLD = 2 # 1 meter for intersection neighbourhood
53
- SEGMENT_LENGTH = 20 # Distance (meter) from intersection to anchor points
54
-
55
-
56
- class Vertex:
57
- def __init__(self, point, line, line_no, end_no, uid):
58
- self.cost_footprint = None
59
- self.pt_optimized = None
60
- self.centerlines = None
61
- self.anchors = None
62
- self.in_raster = None
63
- self.line_radius = None
64
- self.vertex = {"point": [point.x, point.y], "lines": []}
65
- self.add_line(line, line_no, end_no, uid)
66
-
67
- def add_line(self, line, line_no, end_no, uid):
68
- item = [line, end_no, {"line_no": line_no}]
69
- item = self.add_anchors_to_line(item, uid)
70
- if item:
71
- self.vertex["lines"].append(item)
72
-
73
- @staticmethod
74
- def get_angle(line, end_index):
75
- """
76
- Calculate the angle of the first or last segment
77
- line: ArcPy Polyline
78
- end_index: 0 or -1 of the the line vertices. Consider the multipart.
79
- """
80
- pt = points_in_line(line)
81
-
82
- if end_index == 0:
83
- pt_1 = pt[0]
84
- pt_2 = pt[1]
85
- elif end_index == -1:
86
- pt_1 = pt[-1]
87
- pt_2 = pt[-2]
88
-
89
- delta_x = pt_2.x - pt_1.x
90
- delta_y = pt_2.y - pt_1.y
91
- if np.isclose(pt_1.x, pt_2.x):
92
- angle = np.pi / 2
93
- if delta_y > 0:
94
- angle = np.pi / 2
95
- elif delta_y < 0:
96
- angle = -np.pi / 2
97
- else:
98
- angle = np.arctan(delta_y / delta_x)
99
-
100
- # arctan is in range [-pi/2, pi/2], regulate all angles to [[-pi/2, 3*pi/2]]
101
- if delta_x < 0:
102
- angle += np.pi # the second or fourth quadrant
103
-
104
- return angle
105
-
106
- def add_anchors_to_line(self, line, uid):
107
- """
108
- Append new vertex to vertex group, by calculating distance to existing vertices
109
- An anchor point will be added together with line
110
- """
111
- line[2]["UID"] = uid
112
-
113
- # Calculate anchor point for each vertex
114
- # point = Point(self.vertex["point"][0], self.vertex["point"][1])
115
- point = Point(self.point())
116
- line_string = line[0]
117
- index = line[1]
118
- pts = points_in_line(line_string)
119
-
120
- pt_1 = None
121
- pt_2 = None
122
- if index == 0:
123
- pt_1 = point
124
- pt_2 = pts[1]
125
- elif index == -1:
126
- pt_1 = point
127
- pt_2 = pts[-2]
128
-
129
- # Calculate anchor point
130
- dist_pt = 0.0
131
- if pt_1 and pt_2:
132
- dist_pt = pt_1.distance(pt_2)
133
-
134
- # TODO: check why two points are the same
135
- if np.isclose(dist_pt, 0.0):
136
- print('Points are close, return')
137
- return None
138
-
139
- X = pt_1.x + (pt_2.x - pt_1.x) * SEGMENT_LENGTH / dist_pt
140
- Y = pt_1.y + (pt_2.y - pt_1.y) * SEGMENT_LENGTH / dist_pt
141
- line.insert(-1, [X, Y]) # add anchor point to list (the third element)
142
-
143
- return line
144
-
145
- def generate_anchor_pairs(self):
146
- """
147
- Extend line following outward direction to length of SEGMENT_LENGTH
148
- Use the end point as anchor point.
149
- vertex: input intersection with all related lines
150
- return: one or two pairs of anchors according to numbers of lines intersected.
151
- two pairs anchors return when 3 or 4 lines intersected
152
- one pair anchors return when 1 or 2 lines intersected
153
- """
154
- lines = self.lines()
155
- point = self.point()
156
- slopes = []
157
- for line in lines:
158
- line_seg = line[0]
159
- pt_index = line[1]
160
- slopes.append(self.get_angle(line_seg, pt_index))
161
-
162
- index = 0 # the index of line which paired with first line.
163
- pt_start_1 = None
164
- pt_end_1 = None
165
- pt_start_2 = None
166
- pt_end_2 = None
167
-
168
- if len(slopes) == 4:
169
- # get sort order of angles
170
- index = np.argsort(slopes)
171
-
172
- # first anchor pair (first and third in the sorted array)
173
- pt_start_1 = lines[index[0]][2]
174
- pt_end_1 = lines[index[2]][2]
175
-
176
- pt_start_2 = lines[index[1]][2]
177
- pt_end_2 = lines[index[3]][2]
178
- elif len(slopes) == 3:
179
- # find the largest difference between angles
180
- angle_diff = [abs(slopes[0] - slopes[1]), abs(slopes[0] - slopes[2]), abs(slopes[1] - slopes[2])]
181
- angle_diff_norm = [2 * np.pi - i if i > np.pi else i for i in angle_diff]
182
- index = np.argmax(angle_diff_norm)
183
- pairs = [(0, 1), (0, 2), (1, 2)]
184
- pair = pairs[index]
185
-
186
- # first anchor pair
187
- pt_start_1 = lines[pair[0]][2]
188
- pt_end_1 = lines[pair[1]][2]
189
-
190
- # the rest one index
191
- remain = list({0, 1, 2} - set(pair))[0] # the remaining index
192
-
193
- try:
194
- pt_start_2 = lines[remain][2]
195
- # symmetry point of pt_start_2 regarding vertex["point"]
196
- X = point[0] - (pt_start_2[0] - point[0])
197
- Y = point[1] - (pt_start_2[1] - point[1])
198
- pt_end_2 = [X, Y]
199
- except Exception as e:
200
- print(e)
201
-
202
- # this scenario only use two anchors and find the closest point on least cost path
203
- elif len(slopes) == 2:
204
- pt_start_1 = lines[0][2]
205
- pt_end_1 = lines[1][2]
206
- elif len(slopes) == 1:
207
- pt_start_1 = lines[0][2]
208
- # symmetry point of pt_start_1 regarding vertex["point"]
209
- X = point[0] - (pt_start_1[0] - point[0])
210
- Y = point[1] - (pt_start_1[1] - point[1])
211
- pt_end_1 = [X, Y]
212
-
213
- if not pt_start_1 or not pt_end_1:
214
- print("Anchors not found")
215
-
216
- # if points are outside of cost footprint, set to None
217
- points = [pt_start_1, pt_end_1, pt_start_2, pt_end_2]
218
- for index, pt in enumerate(points):
219
- if pt:
220
- if not self.cost_footprint.contains(Point(pt)):
221
- points[index] = None
222
-
223
- if len(slopes) == 4 or len(slopes) == 3:
224
- if None in points:
225
- return None
226
- else:
227
- return points
228
- elif len(slopes) == 2 or len(slopes) == 1:
229
- if None in (pt_start_1, pt_end_1):
230
- return None
231
- else:
232
- return pt_start_1, pt_end_1
233
-
234
- def optimize(self):
235
- try:
236
- self.anchors = self.generate_anchor_pairs()
237
- except Exception as e:
238
- print(e)
239
-
240
- if not self.anchors:
241
- if BT_DEBUGGING:
242
- print("No anchors retrieved")
243
- return None
244
-
245
- centerline_1 = None
246
- centerline_2 = None
247
- intersection = None
248
-
249
- if CL_USE_SKIMAGE_GRAPH:
250
- find_lc_path = find_least_cost_path_skimage
251
- else:
252
- find_lc_path = find_least_cost_path
253
-
254
- try:
255
- if len(self.anchors) == 4:
256
- seed_line = LineString(self.anchors[0:2])
257
-
258
- raster_clip, out_meta = clip_raster(self.in_raster, seed_line, self.line_radius)
259
- if not HAS_COST_RASTER:
260
- raster_clip, _ = cost_raster(raster_clip, out_meta)
261
-
262
- centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
263
- seed_line = LineString(self.anchors[2:4])
264
-
265
- raster_clip, out_meta = clip_raster(self.in_raster, seed_line, self.line_radius)
266
- if not HAS_COST_RASTER:
267
- raster_clip, _ = cost_raster(raster_clip, out_meta)
268
-
269
- centerline_2 = find_lc_path(raster_clip, out_meta, seed_line)
270
-
271
- if centerline_1 and centerline_2:
272
- intersection = intersection_of_lines(centerline_1, centerline_2)
273
- elif len(self.anchors) == 2:
274
- seed_line = LineString(self.anchors)
275
-
276
- raster_clip, out_meta = clip_raster(self.in_raster, seed_line, self.line_radius)
277
- if not HAS_COST_RASTER:
278
- raster_clip, _ = cost_raster(raster_clip, out_meta)
279
-
280
- centerline_1 = find_lc_path(raster_clip, out_meta, seed_line)
281
-
282
- if centerline_1:
283
- intersection = closest_point_to_line(self.point(), centerline_1)
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 MultiPoint:
289
- intersection = intersection.centroid
290
-
291
- self.centerlines = [centerline_1, centerline_2]
292
- self.pt_optimized = intersection
293
- print(f'Processing vertex {self.point()[0]:.2f}, {self.point()[1]:.2f} done')
294
-
295
- def lines(self):
296
- return self.vertex["lines"]
297
-
298
- def point(self):
299
- return self.vertex["point"]
300
-
301
-
302
- class VertexGrouping:
303
- def __init__(self, callback, in_line, in_raster, line_radius, out_line):
304
- self.in_line = in_line
305
- self.in_raster = in_raster
306
- self.line_radius = float(line_radius)
307
- self.out_line = out_line
308
- self.segment_all = []
309
- self.in_schema = None # input shapefile schema
310
- self.crs = None
311
- self.vertex_grp = []
312
- self.sindex = None
313
-
314
- # calculate cost raster footprint
315
- self.cost_footprint = generate_raster_footprint(self.in_raster, latlon=False)
316
-
317
- @staticmethod
318
- def segments(line_coords):
319
- """
320
- Split LineString to segments at vertices
321
- Parameters
322
- ----------
323
- self :
324
- line_coords :
325
-
326
- Returns
327
- -------
328
-
329
- """
330
- if len(line_coords) == 2:
331
- line = shape({'type': 'LineString', 'coordinates': line_coords})
332
- if not np.isclose(line.length, 0.0):
333
- return [line]
334
- elif len(line_coords) > 2:
335
- seg_list = zip(line_coords[:-1], line_coords[1:])
336
- line_list = [shape({'type': 'LineString', 'coordinates': coords}) for coords in seg_list]
337
- return [line for line in line_list if not np.isclose(line.length, 0.0)]
338
-
339
- return None
340
-
341
- def split_lines(self):
342
- with fiona.open(self.in_line) as open_line_file:
343
- # get input shapefile fields
344
- self.in_schema = open_line_file.meta['schema']
345
- self.in_schema['properties']['BT_UID'] = 'int:10' # add field
346
-
347
- i = 0
348
- self.crs = open_line_file.crs
349
- for line in open_line_file:
350
- props = OrderedDict(line['properties'])
351
- if not line['geometry']:
352
- continue
353
- if line['geometry']['type'] != 'MultiLineString':
354
- props[BT_UID] = i
355
- self.segment_all.append([shape(line['geometry']), props])
356
- i += 1
357
- else:
358
- print('MultiLineString found.')
359
- geoms = shape(line['geometry']).geoms
360
- for item in geoms:
361
- props[BT_UID] = i
362
- self.segment_all.append([shape(item), props])
363
- i += 1
364
-
365
- # split line segments at vertices
366
- input_lines_temp = []
367
- line_no = 0
368
- for line in self.segment_all:
369
- line_segs = self.segments(list(line[0].coords))
370
- if line_segs:
371
- for seg in line_segs:
372
- input_lines_temp.append({'line': shape(seg), 'line_no': line_no, 'prop': line[1],
373
- 'start_visited': False, 'end_visited': False})
374
- line_no += 1
375
-
376
- print_msg('Splitting lines', line_no, len(self.segment_all))
377
-
378
- self.segment_all = input_lines_temp
379
-
380
- # create spatial index for all line segments
381
- self.sindex = STRtree([item['line'] for item in self.segment_all])
382
-
383
- def create_vertex_group(self, point, line, line_no, end_no, uid):
384
- """
385
-
386
- Parameters
387
- ----------
388
- point :
389
- line :
390
- end_no : head or tail of line, 0, -1
391
-
392
- Returns
393
- -------
394
-
395
- """
396
- # all end points not added will be stay with this vertex
397
- vertex = Vertex(point, line, line_no, end_no, uid)
398
- search = self.sindex.query(point.buffer(CL_POLYGON_BUFFER))
399
-
400
- # add more vertices to the new group
401
- for i in search:
402
- seg = self.segment_all[i]
403
- if line_no == seg['line_no']:
404
- continue
405
-
406
- uid = seg['prop']['BT_UID']
407
- if not seg['start_visited']:
408
- if self.points_are_close(point, Point(seg['line'].coords[0])):
409
- vertex.add_line(seg['line'], seg['line_no'], 0, uid)
410
- seg['start_visited'] = True
411
-
412
- if not seg['end_visited']:
413
- if self.points_are_close(point, Point(seg['line'].coords[-1])):
414
- vertex.add_line(seg['line'], seg['line_no'], -1, uid)
415
- seg['end_visited'] = True
416
-
417
- vertex.in_raster = self.in_raster
418
- if not HAS_COST_RASTER:
419
- vertex.in_raster = self.in_raster
420
-
421
- vertex.line_radius = self.line_radius
422
- vertex.cost_footprint = self.cost_footprint
423
- self.vertex_grp.append(vertex)
424
-
425
- @staticmethod
426
- def points_are_close(pt1, pt2):
427
- if abs(pt1.x - pt2.x) < DISTANCE_THRESHOLD and abs(pt1.y - pt2.y) < DISTANCE_THRESHOLD:
428
- return True
429
- else:
430
- return False
431
-
432
- def group_vertices(self):
433
- try:
434
- self.split_lines()
435
- print('split_lines done.')
436
-
437
- i = 0
438
- for line in self.segment_all:
439
- pt_list = points_in_line(line['line'])
440
- if len(pt_list) == 0:
441
- print(f"Line {line['line_no']} is empty")
442
- continue
443
- uid = line['prop']['BT_UID']
444
- if not line['start_visited']:
445
- self.create_vertex_group(pt_list[0], line['line'], line['line_no'], 0, uid)
446
- line['start_visited'] = True
447
- i += 1
448
- print_msg('Grouping vertices', i, len(self.segment_all))
449
-
450
- if not line['end_visited']:
451
- self.create_vertex_group(pt_list[-1], line['line'], line['line_no'], -1, uid)
452
- line['end_visited'] = True
453
- i += 1
454
- print_msg('Grouping vertices', i, len(self.segment_all))
455
-
456
- print('group_intersections done.')
457
-
458
- except Exception as e:
459
- print(e)
460
-
461
-
462
- def points_in_line(line):
463
- point_list = []
464
- try:
465
- for point in list(line.coords): # loops through every point in a line
466
- # loops through every vertex of every segment
467
- if point: # adds all the vertices to segment_list, which creates an array
468
- point_list.append(Point(point[0], point[1]))
469
- except Exception as e:
470
- print(e)
471
-
472
- return point_list
473
-
474
-
475
- def update_line_vertex(line, index, point):
476
- if not line:
477
- return None
478
-
479
- if index >= len(line.coords) or index < -1:
480
- return line
481
-
482
- coords = list(line.coords)
483
- if len(coords[index]) == 2:
484
- coords[index] = (point.x, point.y)
485
- elif len(coords[index]) == 3:
486
- coords[index] = (point.x, point.y, 0.0)
487
-
488
- return LineString(coords)
489
-
490
-
491
- def intersection_of_lines(line_1, line_2):
492
- """
493
- only LINESTRING is dealt with for now
494
- Parameters
495
- ----------
496
- line_1 :
497
- line_2 :
498
-
499
- Returns
500
- -------
501
-
502
- """
503
- # intersection collection, may contain points and lines
504
- inter = None
505
- if line_1 and line_2:
506
- inter = line_1.intersection(line_2)
507
-
508
- # TODO: intersection may return GeometryCollection, LineString or MultiLineString
509
- if inter:
510
- if (type(inter) is GeometryCollection or
511
- type(inter) is LineString or
512
- type(inter) is MultiLineString):
513
- return inter.centroid
514
-
515
- return inter
516
-
517
-
518
- def closest_point_to_line(point, line):
519
- if not line:
520
- return None
521
-
522
- pt = line.interpolate(line.project(Point(point)))
523
- return pt
524
-
525
-
526
- def process_single_line(vertex):
527
- """
528
- It uses memory workspace instead of shapefiles.
529
- The refactoring is to accelerate the processing speed.
530
- vertex: intersection with all lines crossed at the intersection
531
- return: optimized vertex
532
- """
533
- vertex.optimize()
534
- return vertex
535
-
536
-
537
- def vertex_optimization(callback, in_line, in_raster, line_radius, out_line, processes, verbose):
538
- if not compare_crs(vector_crs(in_line), raster_crs(in_raster)):
539
- return
540
-
541
- vg = VertexGrouping(callback, in_line, in_raster, line_radius, out_line)
542
- vg.group_vertices()
543
-
544
- vertices = execute_multiprocessing(process_single_line, vg.vertex_grp, 'Vertex Optimization',
545
- processes, 1, verbose=verbose)
546
-
547
- # No line generated, exit
548
- if len(vertices) <= 0:
549
- print("No lines optimized.")
550
- return
551
-
552
- # Flatten vertices which is a list of list
553
- anchor_list = []
554
- leastcost_list = []
555
- inter_list = []
556
- cl_list = []
557
-
558
- # Dump all polylines into point array for vertex updates
559
- feature_all = {}
560
- for i in vg.segment_all:
561
- feature = [i['line'], i['prop']]
562
- feature_all[i['line_no']] = feature
563
-
564
- for vertex in vertices:
565
- if not vertex:
566
- continue
567
-
568
- if vertex.anchors:
569
- for pt in vertex.anchors:
570
- anchor_list.append(Point(pt))
571
-
572
- if vertex.centerlines:
573
- for line in vertex.centerlines:
574
- if line:
575
- leastcost_list.append(line)
576
-
577
- if vertex.pt_optimized:
578
- inter_list.append(vertex.pt_optimized)
579
-
580
- for line in vertex.lines():
581
- index = line[1]
582
- line_no = line[3]["line_no"]
583
- pt_array = feature_all[line_no][0]
584
-
585
- if not pt_array or not vertex.pt_optimized:
586
- continue
587
-
588
- new_intersection = vertex.pt_optimized
589
-
590
- updated_line = pt_array
591
- if index == 0 or index == -1:
592
- try:
593
- updated_line = update_line_vertex(pt_array, index, new_intersection)
594
- except Exception as e:
595
- print(e)
596
-
597
- feature_all[line_no][0] = updated_line
598
-
599
- line_path = Path(out_line)
600
- file_name = line_path.stem
601
- file_line = line_path.as_posix()
602
- file_lc = line_path.with_stem(file_name + '_leastcost').as_posix()
603
- file_anchors = line_path.with_stem(file_name + "_anchors").as_posix()
604
- file_inter = line_path.with_stem(file_name + "_intersections").as_posix()
605
-
606
- fields = []
607
- properties = []
608
- all_lines = [value[0] for key, value in feature_all.items()]
609
- all_props = [value[1] for key, value in feature_all.items()]
610
- save_features_to_shapefile(file_line, vg.crs, all_lines, all_props, vg.in_schema)
611
- save_features_to_shapefile(file_lc, vg.crs, leastcost_list, properties, fields)
612
- save_features_to_shapefile(file_anchors, vg.crs, anchor_list, properties, fields)
613
- save_features_to_shapefile(file_inter, vg.crs, inter_list, properties, fields)
614
-
615
-
616
- if __name__ == '__main__':
617
- in_args, in_verbose = check_arguments()
618
- start_time = time.time()
619
- vertex_optimization(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
620
- print('Elapsed time: {}'.format(time.time() - start_time))
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 the public interface for vertex optimization.
14
+ """
15
+
16
+ import time
17
+
18
+ import beratools.core.algo_vertex_optimization as bt_vo
19
+ import beratools.tools.common as bt_common
20
+
21
+
22
+ def vertex_optimization(
23
+ in_line,
24
+ in_raster,
25
+ search_distance,
26
+ line_radius,
27
+ out_line,
28
+ processes,
29
+ verbose,
30
+ in_layer=None,
31
+ out_layer=None,
32
+ ):
33
+ if not bt_common.compare_crs(
34
+ bt_common.vector_crs(in_line), bt_common.raster_crs(in_raster)
35
+ ):
36
+ return
37
+
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
+ )
49
+ vg.create_all_vertex_groups()
50
+ vg.compute()
51
+ vg.update_all_lines()
52
+ vg.save_all_layers(out_line)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ in_args, in_verbose = bt_common.check_arguments()
57
+ start_time = time.time()
58
+ vertex_optimization(
59
+ **in_args.input, processes=int(in_args.processes), verbose=in_verbose
60
+ )
61
+ print("Elapsed time: {}".format(time.time() - start_time))