BERATools 0.2.0__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 +9 -0
- beratools/core/__init__.py +0 -0
- beratools/core/algo_centerline.py +351 -0
- beratools/core/constants.py +86 -0
- beratools/core/dijkstra_algorithm.py +460 -0
- beratools/core/logger.py +85 -0
- beratools/core/tool_base.py +133 -0
- beratools/gui/__init__.py +15 -0
- beratools/gui/batch_processing_dlg.py +463 -0
- beratools/gui/beratools.json +2300 -0
- beratools/gui/bt_data.py +487 -0
- beratools/gui/bt_gui_main.py +691 -0
- beratools/gui/cli.py +18 -0
- beratools/gui/gui.json +8 -0
- beratools/gui/img/BERALogo.png +0 -0
- beratools/gui/img/closed.gif +0 -0
- beratools/gui/img/closed.png +0 -0
- beratools/gui/img/open.gif +0 -0
- beratools/gui/img/open.png +0 -0
- beratools/gui/img/tool.gif +0 -0
- beratools/gui/img/tool.png +0 -0
- beratools/gui/map_window.py +146 -0
- beratools/gui/tool_widgets.py +493 -0
- beratools/gui_tk/ASCII Banners.txt +248 -0
- beratools/gui_tk/__init__.py +20 -0
- beratools/gui_tk/beratools_main.py +515 -0
- beratools/gui_tk/bt_widgets.py +442 -0
- beratools/gui_tk/cli.py +18 -0
- beratools/gui_tk/gui.json +8 -0
- 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 +14 -0
- beratools/gui_tk/map_window.py +144 -0
- beratools/gui_tk/runner.py +1481 -0
- beratools/gui_tk/tooltip.py +55 -0
- beratools/third_party/pyqtlet2/__init__.py +9 -0
- beratools/third_party/pyqtlet2/leaflet/__init__.py +26 -0
- beratools/third_party/pyqtlet2/leaflet/control/__init__.py +6 -0
- beratools/third_party/pyqtlet2/leaflet/control/control.py +59 -0
- beratools/third_party/pyqtlet2/leaflet/control/draw.py +52 -0
- beratools/third_party/pyqtlet2/leaflet/control/layers.py +20 -0
- beratools/third_party/pyqtlet2/leaflet/core/Parser.py +24 -0
- beratools/third_party/pyqtlet2/leaflet/core/__init__.py +2 -0
- beratools/third_party/pyqtlet2/leaflet/core/evented.py +180 -0
- beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +34 -0
- beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +30 -0
- beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/layer.py +105 -0
- beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +45 -0
- beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +91 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +2 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +4 -0
- beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +16 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +15 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +5 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +14 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +18 -0
- beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +14 -0
- beratools/third_party/pyqtlet2/leaflet/map/__init__.py +1 -0
- beratools/third_party/pyqtlet2/leaflet/map/map.py +220 -0
- beratools/third_party/pyqtlet2/mapwidget.py +45 -0
- beratools/third_party/pyqtlet2/web/custom.js +43 -0
- beratools/third_party/pyqtlet2/web/map.html +23 -0
- 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 +656 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +6 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +14 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +4 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +22 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +43 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +20 -0
- 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 +156 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +10 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +10 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +22 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +57 -0
- beratools/tools/Beratools_r_script.r +1120 -0
- beratools/tools/Ht_metrics.py +116 -0
- beratools/tools/__init__.py +7 -0
- beratools/tools/batch_processing.py +132 -0
- beratools/tools/canopy_threshold_relative.py +670 -0
- beratools/tools/canopycostraster.py +222 -0
- beratools/tools/centerline.py +176 -0
- beratools/tools/common.py +885 -0
- beratools/tools/fl_regen_csf.py +428 -0
- beratools/tools/forest_line_attributes.py +408 -0
- beratools/tools/forest_line_ecosite.py +216 -0
- beratools/tools/lapis_all.py +103 -0
- beratools/tools/least_cost_path_from_chm.py +152 -0
- beratools/tools/line_footprint_absolute.py +363 -0
- beratools/tools/line_footprint_fixed.py +282 -0
- beratools/tools/line_footprint_functions.py +720 -0
- beratools/tools/line_footprint_relative.py +64 -0
- beratools/tools/ln_relative_metrics.py +615 -0
- beratools/tools/r_cal_lpi_elai.r +25 -0
- beratools/tools/r_generate_pd_focalraster.r +101 -0
- beratools/tools/r_interface.py +80 -0
- beratools/tools/r_point_density.r +9 -0
- beratools/tools/rpy_chm2trees.py +86 -0
- beratools/tools/rpy_dsm_chm_by.py +81 -0
- beratools/tools/rpy_dtm_by.py +63 -0
- beratools/tools/rpy_find_cellsize.py +43 -0
- beratools/tools/rpy_gnd_csf.py +74 -0
- beratools/tools/rpy_hummock_hollow.py +85 -0
- beratools/tools/rpy_hummock_hollow_raster.py +71 -0
- beratools/tools/rpy_las_info.py +51 -0
- beratools/tools/rpy_laz2las.py +40 -0
- beratools/tools/rpy_lpi_elai_lascat.py +466 -0
- beratools/tools/rpy_normalized_lidar_by.py +56 -0
- beratools/tools/rpy_percent_above_dbh.py +80 -0
- beratools/tools/rpy_points2trees.py +88 -0
- beratools/tools/rpy_vegcoverage.py +94 -0
- beratools/tools/tiler.py +206 -0
- beratools/tools/tool_template.py +54 -0
- beratools/tools/vertex_optimization.py +620 -0
- beratools/tools/zonal_threshold.py +144 -0
- beratools-0.2.0.dist-info/METADATA +63 -0
- beratools-0.2.0.dist-info/RECORD +142 -0
- beratools-0.2.0.dist-info/WHEEL +4 -0
- beratools-0.2.0.dist-info/entry_points.txt +2 -0
- beratools-0.2.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,620 @@
|
|
|
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))
|