Pamhyr2/src/View/Geometry/Window.py

666 lines
20 KiB
Python

# Window.py -- Pamhyr
# Copyright (C) 2023-2024 INRAE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# -*- coding: utf-8 -*-
import os
import sys
import time
import pathlib
import logging
from copy import deepcopy
from tools import timer, trace, logger_exception
from PyQt5 import QtWidgets
from PyQt5.QtGui import (
QKeySequence,
)
from PyQt5.QtCore import (
QModelIndex, Qt, QSettings, pyqtSlot,
QItemSelectionModel, QCoreApplication, QSize,
)
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QFileDialog, QCheckBox,
QUndoStack, QShortcut, QTableView, QHeaderView,
QAction, QSlider, QPushButton, QVBoxLayout,
QLabel, QAbstractItemView,
)
from Modules import Modules
from Model.Except import ExternFileMissingError
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Tools.Plot.PamhyrCanvas import MplCanvas
from Meshing.Mage import (
MeshingWithMage, MeshingWithMageMailleurTT
)
from View.Geometry.Table import GeometryReachTableModel
from View.Geometry.PlotXY import PlotXY
from View.Geometry.PlotAC import PlotAC
from View.Geometry.PlotKPZ import PlotKPZ
from View.Geometry.MeshingDialog import MeshingDialog
from View.Geometry.Translate import GeometryTranslate
from View.Geometry.Profile.Window import ProfileWindow
_translate = QCoreApplication.translate
logger = logging.getLogger()
class GeometryWindow(PamhyrWindow):
_pamhyr_ui = "GeometryReach"
_pamhyr_name = "Geometry"
def __init__(self, reach=None, study=None, config=None, parent=None):
trad = GeometryTranslate()
if reach is None:
self._reach = study.river.current_reach().reach
else:
self._reach = reach
name = trad[self._pamhyr_name] + f" - {self._reach.name}"
super(GeometryWindow, self).__init__(
title=name,
study=study,
config=config,
trad=trad,
parent=parent
)
# Add reach to hash computation data
self._hash_data.append(self._reach)
self._table = None
self._profile_window = []
self.setup_table()
self.setup_plots()
self.setup_statusbar()
self.setup_connections()
self.changed_slider_value()
def setup_table(self):
table_headers = self._trad.get_dict("table_headers")
table = self.find(QTableView, "tableView")
self._table = GeometryReachTableModel(
table_view=table,
table_headers=table_headers,
editable_headers=["name", "kp"],
data=self._reach,
undo=self._undo_stack
)
table.setModel(self._table)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_plots(self):
self.setup_plots_xy()
self.setup_plots_kpc()
self.setup_plots_ac()
def setup_plots_xy(self):
self._canvas_xy = MplCanvas(width=3, height=4, dpi=100)
self._canvas_xy.setObjectName("canvas_xy")
self._toolbar_xy = PamhyrPlotToolbar(
self._canvas_xy, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self._plot_layout_xy = self.find(QVBoxLayout, "verticalLayout")
self._plot_layout_xy.addWidget(self._toolbar_xy)
self._plot_layout_xy.addWidget(self._canvas_xy)
self.plot_xy()
def setup_plots_kpc(self):
self._canvas_kpc = MplCanvas(width=6, height=4, dpi=100)
self._canvas_kpc.setObjectName("canvas_kpc")
self._toolbar_kpc = PamhyrPlotToolbar(
self._canvas_kpc, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self._plot_layout_kpc = self.find(QVBoxLayout, "verticalLayout_2")
self._plot_layout_kpc.addWidget(self._toolbar_kpc)
self._plot_layout_kpc.addWidget(self._canvas_kpc)
self.plot_kpc()
def setup_plots_ac(self):
self._canvas_ac = MplCanvas(width=9, height=4, dpi=100)
self._canvas_ac.setObjectName("canvas_ac")
self._toolbar_ac = PamhyrPlotToolbar(
self._canvas_ac, self,
items=["home", "zoom", "save", "iso", "back/forward", "move"]
)
self._plot_layout_ac = self.find(QVBoxLayout, "verticalLayout_3")
self._plot_layout_ac.addWidget(self._toolbar_ac)
self._plot_layout_ac.addWidget(self._canvas_ac)
self.plot_ac()
def _compute_status_label(self):
row = self.index_selected_row()
profile = self._reach.profile(row)
name = profile.name + " " + str(profile.kp)
return (
"<font color=\"Grey\">" +
f"{self._trad['reach']}: {self._reach.name}" +
" - " +
f"{self._trad['cross_section']}:</font> {name}"
)
def setup_statusbar(self):
txt = ""
self._status_label = QLabel(txt)
self.statusbar.addPermanentWidget(self._status_label)
def update_statusbar(self):
txt = self._compute_status_label()
self._status_label.setText(txt)
def setup_connections(self):
actions = {
"action_import": self.import_from_file,
"action_export": self.export_to_file,
"action_sort_asc": self.sort_ascending,
"action_sort_des": self.sort_descending,
"action_up": self.move_up,
"action_down": self.move_down,
"action_add": self.add,
"action_delete": self.delete,
"action_edit": self.edit_profile,
"action_meshing": self.edit_meshing,
"action_update_kp": self.update_kp,
"action_purge": self.purge,
}
for action in actions:
self.find(QAction, action)\
.triggered.connect(actions[action])
self.find(QSlider, "verticalSlider").valueChanged.connect(
self.changed_slider_value)
self.find(QPushButton, "pushButton_up").clicked.connect(
self.decrement_value_slider)
self.find(QPushButton, "pushButton_down").clicked.connect(
self.increment_value_slider)
# Profile selection when line change in table
self.find(QTableView, "tableView").selectionModel()\
.selectionChanged\
.connect(self.select_current_profile)
self._table.layoutChanged.connect(self.update_redraw)
def update(self):
self._update()
def update_redraw(self):
self._update(redraw=True)
def _update(self, redraw=False, propagate=True):
if redraw:
self._plot_xy.draw()
self._plot_kpc.draw()
self._plot_ac.draw()
self.select_current_profile()
self.changed_slider_value()
if propagate:
self._propagate_update(key=Modules.GEOMETRY)
def _propagated_update(self, key=Modules(0)):
if Modules.NETWORK not in key and Modules.GEOMETRY not in key:
return
self._update(propagate=False)
def import_from_file(self):
options = QFileDialog.Options()
settings = QSettings(QSettings.IniFormat,
QSettings.UserScope, 'MyOrg', )
options |= QFileDialog.DontUseNativeDialog
file_types = [
self._trad["file_st"],
self._trad["file_m"],
self._trad["file_shp"],
self._trad["file_all"],
]
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
self._trad["open_file"],
"",
";; ".join(file_types),
options=options
)
if filename != "":
size = os.stat(filename).st_size
self._table.import_geometry(0, filename)
def edit_profile(self):
self.tableView.model().blockSignals(True)
rows = list(
set(
(i.row() for i in self.tableView.selectedIndexes())
)
)
for row in rows:
profile = self._reach.profile(row)
if self.sub_window_exists(
ProfileWindow,
data=[None, None, profile]
):
continue
win = ProfileWindow(
profile=profile,
parent=self,
)
self._profile_window.append(win)
win.show()
self.tableView.model().blockSignals(False)
def edit_meshing(self):
try:
dlg = MeshingDialog(
reach=self._reach,
trad=self._trad,
parent=self
)
if dlg.exec():
data = {
"step": dlg.space_step,
"limites": [dlg.begin_cs, dlg.end_cs],
"origin": dlg.origin,
"directrices": [dlg.begin_dir, dlg.end_dir],
"lplan": dlg.lplan,
"lm": dlg.lm,
"linear": dlg.linear,
}
self._edit_meshing(data)
except Exception as e:
logger_exception(e)
return
def _edit_meshing(self, data):
try:
mesher = MeshingWithMageMailleurTT()
self._table.meshing(mesher, data)
except Exception as e:
logger_exception(e)
raise ExternFileMissingError(
module="mage",
filename="MailleurTT",
path=MeshingWithMageMailleurTT._path(),
src_except=e
)
pyqtSlot(bool)
def changed_profile_slot(self, status):
self.update_view1 = status
def plot_xy(self):
self.tableView.model().blockSignals(True)
self._plot_xy = PlotXY(
canvas=self._canvas_xy,
data=self._reach,
trad=self._trad,
toolbar=self._toolbar_xy
)
self._plot_xy.draw()
self.tableView.model().blockSignals(False)
def update_plot_xy(self):
self.tableView.model().blockSignals(True)
self._plot_xy.update()
self.tableView.model().blockSignals(False)
def plot_kpc(self):
self.tableView.model().blockSignals(True)
self._plot_kpc = PlotKPZ(
canvas=self._canvas_kpc,
data=self._reach,
trad=self._trad,
toolbar=self._toolbar_kpc
)
self._plot_kpc.draw()
self.tableView.model().blockSignals(False)
def update_plot_kpc(self):
self.tableView.model().blockSignals(True)
self._plot_kpc.update()
self.tableView.model().blockSignals(False)
def plot_ac(self):
self.tableView.model().blockSignals(True)
self._plot_ac = PlotAC(
canvas=self._canvas_ac,
data=self._reach,
trad=self._trad,
toolbar=self._toolbar_ac,
)
self._plot_ac.draw()
self.tableView.model().blockSignals(False)
def update_plot_ac(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_ac.current = ind
self._plot_ac.update()
self.tableView.model().blockSignals(False)
def get_station(self, ind: int):
return self._reach.profile(ind).get_station()
def get_elevation(self, ind: int):
return self._reach.profile(ind).z()
def select_plot_xy(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_xy.current = ind
self._plot_xy.update()
self.tableView.model().blockSignals(False)
def select_plot_kpc(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_kpc.current = ind
self._plot_kpc.update()
self.tableView.model().blockSignals(False)
def select_plot_ac(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_ac.current = ind
self._plot_ac.update()
self.tableView.model().blockSignals(False)
def select_row_profile_slider(self, ind: int = 0):
if self.tableView is not None:
selectionModel = self.tableView.selectionModel()
index = self.tableView.model().index(ind, 0)
selectionModel.select(
index,
QItemSelectionModel.Rows |
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Select
)
self.tableView.scrollTo(index)
def select_current_profile(self):
self.tableView.model().blockSignals(True)
if len(self.tableView.selectedIndexes()) > 0:
row = self.index_selected_row()
self.find(QSlider, "verticalSlider").setValue(row)
self.select_plot_xy(row)
self.select_plot_kpc(row)
self.select_plot_ac(row)
else:
self._plot_xy.draw()
self._plot_kpc.draw()
self._plot_ac.draw()
self.tableView.model().blockSignals(False)
def changed_slider_value(self):
self.tableView.model().blockSignals(True)
if self._table.rowCount() != 0:
slider = self.find(QSlider, "verticalSlider")
slider.setMaximum(self._table.rowCount() - 1)
slider_value = slider.value()
kp = self._reach.profile(slider_value).kp
self.select_plot_xy(slider_value)
self.select_plot_kpc(slider_value)
self.select_row_profile_slider(slider_value)
self.update_statusbar()
self.tableView.model().blockSignals(False)
def increment_value_slider(self):
slider = self.find(QSlider, "verticalSlider")
if 0 <= slider.value() < self._table.rowCount() - 1:
slider.setValue(slider.value() + 1)
def decrement_value_slider(self):
slider = self.find(QSlider, "verticalSlider")
if 0 < slider.value() < self._table.rowCount():
slider.setValue(slider.value() - 1)
def add(self):
if len(self.tableView.selectedIndexes()) == 0:
self._table.add(self._table.rowCount())
else:
row = self.index_selected_row()
self._table.add(row + 1)
def delete(self):
rows = sorted(
list(
set(
[index.row() for index in self.tableView.selectedIndexes()]
)
)
)
if len(rows) > 0:
self._table.delete(rows)
def index_selected_row(self):
return self.tableView\
.selectionModel()\
.selectedRows()[0]\
.row()
def sort_ascending(self):
self._table.sort_profiles(False)
self.select_current_profile()
self.changed_slider_value()
def sort_descending(self):
self._table.sort_profiles(True)
self.select_current_profile()
self.changed_slider_value()
def move_up(self):
row = self.index_selected_row()
self._table.move_up(row)
self.select_current_profile()
def move_down(self):
row = self.index_selected_row()
self._table.move_down(row)
self.select_current_profile()
def update_kp(self):
pass
def purge(self):
self._table.purge()
self.update_redraw()
def duplicate(self):
rows = [
row.row() for row in
self.tableView.selectionModel().selectedRows()
]
profiles = []
for row in rows:
profiles.append(
self._reach.profile(row)
)
if len(profiles) == 0:
return
self._table.duplicate(rows, profiles)
self.select_current_profile()
def _copy(self):
rows = self.tableView\
.selectionModel()\
.selectedRows()
table = []
# table.append(["name", "kp"])
for row in rows:
profile = self._reach.profile(row.row())
table.append(
[profile.name, profile.kp]
)
self.copyTableIntoClipboard(table)
def _paste(self):
header, data = self.parseClipboardTable()
if len(data) + len(header) == 0:
return
# if len(header) != 0:
# header.append("reach")
# header.append("status")
# HACK: The CVS module detect the first line to csv header in
# some particular case... To avoid this we append to data list
# the "headers". /!\ This hack must failed if a real header
# exists (a better solution is welcome).
logger.debug(
"Geometry: Paste: " +
f"header = {header}, " +
f"data = {data}"
)
if len(header) != 0:
data = [header] + data
try:
for row in data:
row.append(self._reach)
row.append(self._study.river._status)
row = self.index_selected_row()
# self._table.paste(row, header, data)
self._table.paste(row, [], data)
self.select_current_profile()
except Exception as e:
logger_exception(e)
def _undo(self):
self._table.undo()
self.select_current_profile()
# self.update_plot_ac()
# self.update_plot_xy()
# self.update_plot_kpc()
def _redo(self):
self._table.redo()
self.select_current_profile()
# self.update_plot_ac()
# self.update_plot_xy()
# self.update_plot_kpc()
def export_to_file(self):
settings = QSettings(
QSettings.IniFormat,
QSettings.UserScope, 'MyOrg'
)
if self._study.filename != "" or self._study.filename is not None:
default_directory = os.path.basename(self._study.filename)
current_dir = settings.value(
'current_directory',
default_directory,
type=str
)
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filename, filters = QFileDialog.getSaveFileName(
self,
filter=(
self._trad["file_st"] + ";; " +
self._trad["file_all"]
),
options=options
)
if filename != '' and filename is not None:
self._export_to_file_st(filename)
def _export_to_file_st(self, filename):
with open(filename, "w+") as f:
f.write("# Exported from Pamhyr2\n")
self._export_to_file_st_reach(f, self._reach)
def _export_to_file_st_reach(self, wfile, reach):
pid = 0
for profile in reach.profiles:
self._export_to_file_st_profile(wfile, profile, pid)
pid += 1
def _export_to_file_st_profile(self, wfile, profile, pid):
num = f"{pid:>6}"
c1 = f"{profile.code1:>6}"
c2 = f"{profile.code2:>6}"
t = f"{len(profile.points):>6}"
kp = f"{profile.kp:>12f}"[0:12]
pname = profile.name
if profile.name == "":
pname = f"p{profile.id:>3}".replace(" ", "0")
name = f"{pname:<19}"
wfile.write(f"{num}{c1}{c2}{t} {kp} {pname}\n")
for point in profile.points:
self._export_to_file_st_point(wfile, point)
wfile.write(f" 999.9990 999.9990 999.9990\n")
def _export_to_file_st_point(self, wfile, point):
x = f"{point.x:<12.4f}"[0:12]
y = f"{point.y:<12.4f}"[0:12]
z = f"{point.z:<12.4f}"[0:12]
n = f"{point.name:<3}"
wfile.write(f"{x} {y} {z} {n}\n")