# Window.py -- Pamhyr
# Copyright (C) 2023 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 .
# -*- coding: utf-8 -*-
import os
import pathlib
import sys
import time
from copy import deepcopy
from tools import timer, trace
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,
)
from View.Tools.PamhyrWindow import PamhyrWindow
from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar
from View.Tools.Plot.PamhyrCanvas import MplCanvas
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.Translate import GeometryTranslate
from View.Geometry.Profile.Window import ProfileWindow
_translate = QCoreApplication.translate
class GeometryWindow(PamhyrWindow):
_pamhyr_ui = "GeometryReach"
_pamhyr_name = "Geometry"
def __init__(self, reach=None, study=None, config=None, parent=None):
if reach is None:
self._reach = study.river.current_reach().reach
else:
self._reach = reach
name = f"{self._pamhyr_name} - {self._reach.name}"
super(GeometryWindow, self).__init__(
title = name,
study = study,
config = config,
trad = GeometryTranslate(),
parent = parent
)
self._tablemodel = 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._tablemodel = GeometryReachTableModel(
table_view = table,
table_headers = table_headers,
editable_headers = ["name", "kp"],
data = self._reach,
undo = self._undo_stack
)
table.setModel(self._tablemodel)
table.setSelectionBehavior(QAbstractItemView.SelectRows)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.setAlternatingRowColors(True)
def setup_plots(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()
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()
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 (
f"{self._trad['reach']}: {self._reach.name}" + " - "
f"{self._trad['cross_section']}: {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,
}
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)
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_all"],
]
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
self._trad["open_file"],
"",
";; ".join(file_types),
options=options
)
if filename != "":
size = os.stat(filename).st_size
self._reach.import_geometry(filename)
self._tablemodel.layoutChanged.emit()
self.update_profile_windows()
self.plot_xy()
self.plot_kpc()
self.plot_ac()
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)
win = self.sub_win_filter_first(
"Profile",
contain = [self._reach.name, str(profile.kp)]
)
if win is None:
win = ProfileWindow(
profile = profile,
parent = self,
)
self._profile_window.append(win)
win.show()
else:
win.activateWindow()
self.tableView.model().blockSignals(False)
pyqtSlot(bool)
def changed_profile_slot(self, status):
self.update_view1 = status
def update_profile_windows(self):
self.list_second_window = []
self.list_row = []
def plot_xy(self):
self.tableView.model().blockSignals(True)
self._plot_xy = PlotXY(
canvas = self._canvas_xy,
data = self._reach,
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,
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,
toolbar = self._toolbar_ac,
plot_xy = self._plot_xy
)
self._plot_ac.draw()
self.tableView.model().blockSignals(False)
def update_plot_ac(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_ac.update(ind=ind)
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.update(ind=ind)
self.tableView.model().blockSignals(False)
def select_plot_kpc(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_kpc.update(ind=ind)
self.tableView.model().blockSignals(False)
def select_plot_ac(self, ind: int):
self.tableView.model().blockSignals(True)
self._plot_ac.update(ind=ind)
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)
self.tableView.model().blockSignals(False)
def changed_slider_value(self):
self.tableView.model().blockSignals(True)
if self._tablemodel.rowCount() != 0:
slider = self.find(QSlider, "verticalSlider")
slider.setMaximum(self._tablemodel.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._tablemodel.rowCount() - 1:
slider.setValue(slider.value() + 1)
def decrement_value_slider(self):
slider = self.find(QSlider, "verticalSlider")
if 0 < slider.value() < self._tablemodel.rowCount():
slider.setValue(slider.value() - 1)
def add(self):
if len(self.tableView.selectedIndexes()) == 0:
self._tablemodel.add(self._tablemodel.rowCount())
else:
row = self.index_selected_row()
self._tablemodel.add(row + 1)
def delete(self):
rows = sorted(
list(
set(
[index.row() for index in self.tableView.selectedIndexes()]
)
)
)
if len(rows) > 0:
self._tablemodel.remove_rows(rows)
self.update_plot_xy()
self.select_current_profile()
self.plot_kpc()
self.changed_slider_value()
def index_selected_row(self):
return self.tableView\
.selectionModel()\
.selectedRows()[0]\
.row()
def sort_ascending(self):
self._tablemodel.sort_profiles(True)
self.select_current_profile()
self.changed_slider_value()
def sort_descending(self):
self._tablemodel.sort_profiles(False)
self.select_current_profile()
self.changed_slider_value()
def move_up(self):
row = self.index_selected_row()
self._tablemodel.move_up(row)
self.select_current_profile()
def move_down(self):
row = self.index_selected_row()
self._tablemodel.move_down(row)
self.select_current_profile()
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._tablemodel.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) == 0:
return
if len(header) != 0:
header.append("reach")
for row in data:
row.append(self._reach)
row = self.index_selected_row()
self._tablemodel.paste(row, header, data)
self.select_current_profile()
def _undo(self):
self._tablemodel.undo()
self.select_current_profile()
self.update_plot_xy()
self.update_plot_kpc()
def _redo(self):
self._tablemodel.redo()
self.select_current_profile()
self.update_plot_xy()
self.update_plot_kpc()
def export_to_file(self):
options = QFileDialog.Options()
DEFAULT_DIRECTORY = '/home/'
settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', )
current_dir = settings.value('current_directory', DEFAULT_DIRECTORY, type=str)
options |= QFileDialog.DontUseNativeDialog
filename, filters = QFileDialog.getSaveFileName(
self,
filter = self._trad["file_st"] + ";; " + self._trad["file_all"],
options = options
)
current_dir = os.path.split(filename)[0] or DEFAULT_DIRECTORY
if filename != '':
self._tablemodel.export_reach(filename)