430 lines
19 KiB
Python
430 lines
19 KiB
Python
|
|
from PyQt6 import QtWidgets
|
|
from appTool import AppToolEditor
|
|
from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCDoubleSpinner, FCComboBox
|
|
|
|
from shapely import Polygon
|
|
|
|
import gettext
|
|
import appTranslation as fcTranslate
|
|
import builtins
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
|
|
class BufferSelectionTool(AppToolEditor):
|
|
"""
|
|
Simple input for buffer distance.
|
|
"""
|
|
|
|
def __init__(self, app, draw_app):
|
|
AppToolEditor.__init__(self, app)
|
|
|
|
self.draw_app = draw_app
|
|
self.decimals = app.decimals
|
|
|
|
self.ui = BufferEditorUI(layout=self.layout, buffer_class=self)
|
|
|
|
self.connect_signals_at_init()
|
|
self.set_tool_ui()
|
|
|
|
def connect_signals_at_init(self):
|
|
# Signals
|
|
self.ui.buffer_button.clicked.connect(self.on_buffer)
|
|
self.ui.buffer_int_button.clicked.connect(self.on_buffer_int)
|
|
self.ui.buffer_ext_button.clicked.connect(self.on_buffer_ext)
|
|
|
|
def run(self):
|
|
self.app.defaults.report_usage("Geo Editor ToolBuffer()")
|
|
super().run()
|
|
|
|
# if the splitter us hidden, display it
|
|
if self.app.ui.splitter.sizes()[0] == 0:
|
|
self.app.ui.splitter.setSizes([1, 1])
|
|
|
|
# if the Tool Tab is hidden display it, else hide it but only if the objectName is the same
|
|
found_idx = None
|
|
for idx in range(self.app.ui.notebook.count()):
|
|
if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab":
|
|
found_idx = idx
|
|
break
|
|
# show the Tab
|
|
if not found_idx:
|
|
try:
|
|
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
|
|
except RuntimeError:
|
|
self.app.ui.plugin_tab = QtWidgets.QWidget()
|
|
self.app.ui.plugin_tab.setObjectName("plugin_tab")
|
|
self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
|
|
self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
|
|
|
|
self.app.ui.plugin_scroll_area = VerticalScrollArea()
|
|
self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
|
|
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
|
|
|
|
# focus on Tool Tab
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
|
|
|
|
# self.app.ui.notebook.callback_on_close = self.on_tab_close
|
|
|
|
self.app.ui.notebook.setTabText(2, _("Buffer"))
|
|
|
|
def set_tool_ui(self):
|
|
# Init appGUI
|
|
self.ui.buffer_distance_entry.set_value(0.01)
|
|
|
|
def on_tab_close(self):
|
|
self.draw_app.select_tool("select")
|
|
self.app.ui.notebook.callback_on_close = lambda: None
|
|
|
|
def on_buffer(self):
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value())
|
|
except ValueError:
|
|
# try to convert comma to decimal point. if it's still not working error message and return
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.'))
|
|
self.ui.buffer_distance_entry.set_value(buffer_distance)
|
|
except ValueError:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
_("Buffer distance value is missing or wrong format. Add it and retry."))
|
|
return
|
|
# the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
|
|
# I populated the combobox such that the index coincide with the join styles value (which is really an INT)
|
|
join_style = {1: 'round', 2: 'mitre', 3: 'bevel'}.get(self.ui.buffer_corner_cb.currentIndex() + 1)
|
|
self.buffer(buffer_distance, join_style)
|
|
|
|
def on_buffer_int(self):
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value())
|
|
except ValueError:
|
|
# try to convert comma to decimal point. if it's still not working error message and return
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.'))
|
|
self.ui.buffer_distance_entry.set_value(buffer_distance)
|
|
except ValueError:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
_("Buffer distance value is missing or wrong format. Add it and retry."))
|
|
return
|
|
# the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
|
|
# I populated the combobox such that the index coincide with the join styles value (which is really an INT)
|
|
join_style = {1: 'round', 2: 'mitre', 3: 'bevel'}.get(self.ui.buffer_corner_cb.currentIndex() + 1)
|
|
self.buffer_int(buffer_distance, join_style)
|
|
|
|
def on_buffer_ext(self):
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value())
|
|
except ValueError:
|
|
# try to convert comma to decimal point. if it's still not working error message and return
|
|
try:
|
|
buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.'))
|
|
self.ui.buffer_distance_entry.set_value(buffer_distance)
|
|
except ValueError:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
_("Buffer distance value is missing or wrong format. Add it and retry."))
|
|
return
|
|
# the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
|
|
# I populated the combobox such that the index coincide with the join styles value (which is really an INT)
|
|
join_style = {1: 'round', 2: 'mitre', 3: 'bevel'}.get(self.ui.buffer_corner_cb.currentIndex() + 1)
|
|
self.buffer_ext(buffer_distance, join_style)
|
|
|
|
def buffer(self, buf_distance, join_style):
|
|
def work_task(geo_editor):
|
|
with geo_editor.app.proc_container.new(_("Working...")):
|
|
selected = geo_editor.get_selected()
|
|
|
|
if buf_distance < 0:
|
|
msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. "
|
|
"Use Buffer interior to generate an 'inside' shape")
|
|
geo_editor.app.inform.emit(msg)
|
|
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
if len(selected) == 0:
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected."))
|
|
return 'fail'
|
|
|
|
if not isinstance(buf_distance, float):
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance."))
|
|
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
results = []
|
|
usable_resolution = int(int(geo_editor.app.options["geometry_circle_steps"]) / 4)
|
|
for t in selected:
|
|
if not t.geo.is_empty and t.geo.is_valid:
|
|
if t.geo.geom_type == 'Polygon':
|
|
results.append(
|
|
t.geo.exterior.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=usable_resolution,
|
|
join_style=join_style)
|
|
)
|
|
elif t.geo.geom_type == 'MultiLineString':
|
|
for line in t.geo:
|
|
if line.is_ring:
|
|
b_geo = Polygon(line)
|
|
results.append(b_geo.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=usable_resolution,
|
|
join_style=join_style).exterior)
|
|
results.append(b_geo.buffer(
|
|
-buf_distance + 1e-10,
|
|
resolution=usable_resolution,
|
|
join_style=join_style).exterior)
|
|
elif t.geo.geom_type in ['LineString', 'LinearRing']:
|
|
if t.geo.is_ring:
|
|
b_geo = Polygon(t.geo)
|
|
results.append(b_geo.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=usable_resolution,
|
|
join_style=join_style).exterior)
|
|
results.append(b_geo.buffer(
|
|
-buf_distance + 1e-10,
|
|
resolution=usable_resolution,
|
|
join_style=join_style).exterior
|
|
)
|
|
|
|
if not results:
|
|
geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
for sha in results:
|
|
geo_editor.add_shape(sha)
|
|
|
|
geo_editor.plot_all()
|
|
geo_editor.build_ui_sig.emit()
|
|
geo_editor.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]})
|
|
|
|
def buffer_int(self, buf_distance, join_style):
|
|
def work_task(geo_editor):
|
|
with geo_editor.app.proc_container.new(_("Working...")):
|
|
selected = geo_editor.get_selected()
|
|
|
|
if buf_distance < 0:
|
|
geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
if len(selected) == 0:
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected."))
|
|
return 'fail'
|
|
|
|
if not isinstance(buf_distance, float):
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
results = []
|
|
for t in selected:
|
|
if not t.geo.is_empty and t.geo.is_valid:
|
|
if t.geo.geom_type == 'Polygon':
|
|
results.append(t.geo.exterior.buffer(
|
|
-buf_distance + 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
elif t.geo.geom_type == 'MultiLineString':
|
|
for line in t.geo:
|
|
if line.is_ring:
|
|
b_geo = Polygon(line)
|
|
else:
|
|
b_geo = line
|
|
results.append(b_geo.buffer(
|
|
-buf_distance + 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
elif t.geo.geom_type in ['LineString', 'LinearRing']:
|
|
if t.geo.is_ring:
|
|
b_geo = Polygon(t.geo)
|
|
else:
|
|
b_geo = t.geo
|
|
results.append(b_geo.buffer(
|
|
-buf_distance + 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
|
|
if not results:
|
|
geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
for sha in results:
|
|
geo_editor.add_shape(sha)
|
|
|
|
geo_editor.plot_all()
|
|
geo_editor.build_ui_sig.emit()
|
|
geo_editor.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]})
|
|
|
|
def buffer_ext(self, buf_distance, join_style):
|
|
def work_task(geo_editor):
|
|
with geo_editor.app.proc_container.new(_("Working...")):
|
|
selected = geo_editor.get_selected()
|
|
|
|
if buf_distance < 0:
|
|
msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. "
|
|
"Use Buffer interior to generate an 'inside' shape")
|
|
geo_editor.app.inform.emit(msg)
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return
|
|
|
|
if len(selected) == 0:
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected."))
|
|
return
|
|
|
|
if not isinstance(buf_distance, float):
|
|
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return
|
|
|
|
results = []
|
|
for t in selected:
|
|
if not t.geo.is_empty and t.geo.is_valid:
|
|
if t.geo.geom_type == 'Polygon':
|
|
results.append(t.geo.exterior.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
elif t.geo.geom_type == 'MultiLineString':
|
|
for line in t.geo:
|
|
if line.is_ring:
|
|
b_geo = Polygon(line)
|
|
else:
|
|
b_geo = line
|
|
results.append(b_geo.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
elif t.geo.geom_type in ['LineString', 'LinearRing']:
|
|
if t.geo.is_ring:
|
|
b_geo = Polygon(t.geo)
|
|
else:
|
|
b_geo = t.geo
|
|
results.append(b_geo.buffer(
|
|
buf_distance - 1e-10,
|
|
resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4),
|
|
join_style=join_style).exterior
|
|
)
|
|
|
|
if not results:
|
|
geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty."))
|
|
# deselect everything
|
|
geo_editor.selected = []
|
|
geo_editor.plot_all()
|
|
return 'fail'
|
|
|
|
for sha in results:
|
|
geo_editor.add_shape(sha)
|
|
|
|
geo_editor.plot_all()
|
|
geo_editor.build_ui_sig.emit()
|
|
geo_editor.app.inform.emit('[success] %s' % _("Done."))
|
|
|
|
self.app.worker_task.emit({'fcn': work_task, 'params': [self.draw_app]})
|
|
|
|
def hide_tool(self):
|
|
self.ui.buffer_tool_frame.hide()
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
|
|
|
|
|
|
class BufferEditorUI:
|
|
pluginName = _("Buffer")
|
|
|
|
def __init__(self, layout, buffer_class):
|
|
self.buffer_class = buffer_class
|
|
self.decimals = self.buffer_class.app.decimals
|
|
self.layout = layout
|
|
|
|
# Title
|
|
title_label = FCLabel("%s" % ('Editor ' + self.pluginName), size=16, bold=True)
|
|
self.layout.addWidget(title_label)
|
|
|
|
self.param_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True)
|
|
self.layout.addWidget(self.param_label)
|
|
|
|
# this way I can hide/show the frame
|
|
self.buffer_tool_frame = QtWidgets.QFrame()
|
|
self.buffer_tool_frame.setContentsMargins(0, 0, 0, 0)
|
|
self.layout.addWidget(self.buffer_tool_frame)
|
|
|
|
self.buffer_tools_box = QtWidgets.QVBoxLayout()
|
|
self.buffer_tools_box.setContentsMargins(0, 0, 0, 0)
|
|
self.buffer_tool_frame.setLayout(self.buffer_tools_box)
|
|
|
|
# #############################################################################################################
|
|
# Tool Params Frame
|
|
# #############################################################################################################
|
|
tool_par_frame = FCFrame()
|
|
self.buffer_tools_box.addWidget(tool_par_frame)
|
|
|
|
# Grid Layout
|
|
param_grid = GLay(v_spacing=5, h_spacing=3)
|
|
tool_par_frame.setLayout(param_grid)
|
|
|
|
# Buffer distance
|
|
self.buffer_distance_entry = FCDoubleSpinner()
|
|
self.buffer_distance_entry.set_precision(self.decimals)
|
|
self.buffer_distance_entry.set_range(0.0000, 10000.0000)
|
|
param_grid.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0)
|
|
param_grid.addWidget(self.buffer_distance_entry, 0, 1)
|
|
|
|
self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner"))
|
|
self.buffer_corner_lbl.setToolTip(
|
|
_("There are 3 types of corners:\n"
|
|
" - 'Round': the corner is rounded for exterior buffer.\n"
|
|
" - 'Square': the corner is met in a sharp angle for exterior buffer.\n"
|
|
" - 'Beveled': the corner is a line that directly connects the features meeting in the corner")
|
|
)
|
|
self.buffer_corner_cb = FCComboBox()
|
|
self.buffer_corner_cb.addItem(_("Round"))
|
|
self.buffer_corner_cb.addItem(_("Square"))
|
|
self.buffer_corner_cb.addItem(_("Beveled"))
|
|
param_grid.addWidget(self.buffer_corner_lbl, 2, 0)
|
|
param_grid.addWidget(self.buffer_corner_cb, 2, 1)
|
|
|
|
# Buttons
|
|
hlay = QtWidgets.QHBoxLayout()
|
|
self.buffer_tools_box.addLayout(hlay)
|
|
|
|
self.buffer_int_button = FCButton(_("Buffer Interior"))
|
|
hlay.addWidget(self.buffer_int_button)
|
|
self.buffer_ext_button = FCButton(_("Buffer Exterior"))
|
|
hlay.addWidget(self.buffer_ext_button)
|
|
|
|
hlay1 = QtWidgets.QHBoxLayout()
|
|
self.buffer_tools_box.addLayout(hlay1)
|
|
|
|
self.buffer_button = FCButton(_("Full Buffer"))
|
|
hlay1.addWidget(self.buffer_button)
|
|
|
|
self.layout.addStretch(1)
|