# Plot.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 . # -*- coding: utf-8 -*- import logging from math import dist, sqrt from tools import timer, trace from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( Qt, QCoreApplication, QItemSelectionModel, QItemSelection, QItemSelectionRange, ) from PyQt5.QtWidgets import QApplication from matplotlib.widgets import RectangleSelector _translate = QCoreApplication.translate logger = logging.getLogger() class Plot(PamhyrPlot): def __init__(self, canvas=None, trad=None, data=None, toolbar=None, table=None, parent=None): super(Plot, self).__init__( canvas=canvas, trad=trad, data=data, toolbar=toolbar, parent=parent ) self._table = table self._parent = parent self._z_note = None self._z_line = None self._z_fill_between = None self.line_xy = [] self.line_gl = [] self.label_x = self._trad["unit_kp"] self.label_y = self._trad["unit_height"] self.before_plot_selected = None self.plot_selected = None self.after_plot_selected = None self._isometric_axis = False self.hl_points = [] self.highlight = ( [], # Points list to highlight None # Hydrolic values (z, wet_area, # wet_preimeter, water_width) ) self._onpickevent = None self._rect_select = RectangleSelector(ax=self.canvas.axes, onselect=self.rect_select_callback, useblit=True, button=[1], # don't use middle nor right button minspanx=2.0, minspany=2.0, spancoords='pixels', interactive=False) def onrelease(self, event): # we need to do that to prevent conflicst between onpick and rect_select_callback modifiers = QApplication.keyboardModifiers() points, hyd = self.highlight if self._onpickevent is not None: ind, point = self._closest_point(self._onpickevent) if modifiers == Qt.ControlModifier: rows = self._parent.index_selected_rows() if ind in rows: rows.remove(ind) del(points[ind]) self.highlight = (points, hyd) self._select_in_table(rows) else: self.highlight = (points+[point], hyd) self._select_in_table(rows+[ind]) elif modifiers == Qt.ShiftModifier: rows = self._parent.index_selected_rows() if len(rows)>0: i1 = min(rows[0], rows[-1], ind) i2 = max(rows[0], rows[-1], ind) p = [[self.data.points[i].x,self.data.points[i].y] for i in range(i1, i2)] else: i1 = ind i2 = ind p = [point] self.highlight = (p, hyd) self._select_range_in_table(i1, i2) else: self.highlight = ([point], hyd) self._select_in_table([ind]) self._onpickevent = None def onpick(self, event): if event.mouseevent.inaxes != self.canvas.axes: return if event.mouseevent.button.value != 1: return modifiers = QApplication.keyboardModifiers() if modifiers not in [Qt.ControlModifier, Qt.NoModifier, Qt.ShiftModifier]: return self._onpickevent = event return def onclick(self, event): if event.inaxes != self.canvas.axes: return if event.button.value == 1: return points, _ = self.highlight z = self._get_z_from_click(event) if z < self.data.z_min() or event.button.value == 2: self.highlight = (points, None) self.update() return a, p, w = self._compute_hydraulics(z) logger.debug(f"{z, a, p, w}") self.highlight = (points, (z, a, p, w)) self.update() return def select_points_from_indices(self, indices): data = self.data _, hyd = self.highlight points = list( map( lambda e: e[1], filter( lambda e: e[0] in indices, enumerate( zip(data.get_station(), data.z()) ) ) ) ) self.highlight = (points, hyd) self.update() def _select_in_table(self, ind): if self._table is not None: self._table.blockSignals(True) self._table.setFocus() selection = self._table.selectionModel() index = QItemSelection() if len(ind) > 0: for i in ind: index.append(QItemSelectionRange(self._table.model().index(i, 0))) selection.select( index, QItemSelectionModel.Rows | QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Select ) if len(ind) > 0: self._table.scrollTo(self._table.model().index(ind[-1], 0)) self._table.blockSignals(False) def _select_range_in_table(self, ind1, ind2): if self._table is not None: self._table.blockSignals(True) self._table.setFocus() selection = self._table.selectionModel() index = QItemSelection(self._table.model().index(ind1, 0), self._table.model().index(ind2, 0)) selection.select( index, QItemSelectionModel.Rows | QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Select ) self._table.scrollTo(self._table.model().index(ind2, 0)) self._table.blockSignals(False) def _closest_point(self, event): points_ind = event.ind axes = self.canvas.axes bx, by = axes.get_xlim(), axes.get_ylim() ratio = (bx[0] - bx[1]) / (by[0] - by[1]) x = event.artist.get_xdata() y = event.artist.get_ydata() # points = filter( # lambda e: e[0] in points_ind, # enumerate(zip(x, y)) # ) points = enumerate(zip(x, y)) mx = event.mouseevent.xdata my = event.mouseevent.ydata def dist_mouse(point): x, y = point[1] d2 = ((mx - x) / ratio) ** 2 + ((my - y) ** 2) return d2 closest = min( points, key=dist_mouse ) return closest def _get_z_from_click(self, event): return event.ydata def rect_select_callback(self, eclick, erelease): points, hyd = self.highlight x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata if(max(abs(x1-x2), abs(y1-y2))<0.001): return modifiers = QApplication.keyboardModifiers() x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata inds, points2 = self._points_in_rectangle(x1, y1, x2, y2) self._onclickevent = None if modifiers == Qt.ControlModifier: rows = self._parent.index_selected_rows() if all(i in rows for i in inds): for ind in sorted(inds, reverse=True): rows.remove(ind) del(points[ind]) self.highlight = (points, hyd) self._select_in_table(rows) else: self.highlight = (points+points2, hyd) self._select_in_table(rows+inds) else: self.highlight = (points2, hyd) self._select_in_table(inds) return def _points_in_rectangle(self, x1, y1, x2, y2): # TODO: use lambdas listi = [] listp = [] station = self.data._get_station(self.data.points) for i, p in enumerate(self.data.points): if (min(x1,x2)