# mpl_canvas_onpick_event.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 . import logging from time import time import numpy as np from PyQt5 import QtWidgets from PyQt5.QtCore import QItemSelectionModel, Qt from PyQt5.QtWidgets import QApplication from shapely.geometry.polygon import Polygon as ShapelyPolygon logger = logging.getLogger() class OnpickEvent(object): def __init__(self, ax, x, y, x_carto, y_carto, tableview=None): """ Args: ax: objet Axes. x: abscisse x1 du graphe (f(x1) = y1) à tracer. y: ordonnée y1 du graphe (f(x1) = y1) à tracer. x_carto: (vecteur) abscisse des points (X,Y,Z) du profil. y_carto: (vecteur) abscisse des points (X,Y,Z) du profil tableview: tableau (de type QtableView) 'associé' au grahique. """ self.ax = ax self.x = x self.y = y self.x_carto = x_carto self.y_carto = y_carto self.tableView = tableview self.counter_onclick = 0 # self.select_point_plot() self.count = 0 self.annotation_onclick = self.ax.annotate( "", xytext=(np.mean(self.x), np.mean(self.y)), xy=(np.mean(self.x), np.mean(self.y)), horizontalalignment='center', fontsize=8, # fontstyle='italic', fontweight='bold', alpha=0.7 ) self.annotation_onclick.set_visible(False) self.pos_x = 0 self.zomm_xmin_xmax = self.ax.get_xlim() self.plot_selec() # self.select_point_plot() self._largeur_miroir, = self.ax.plot( self.x[1], self.y[1], color='blue', lw=1.2, ls=":" ) self.pt = [] self.tableView.selectionModel()\ .selectionChanged\ .connect(self.update_select_point_point) def select_row_pt_clicked(self, ind: int = 0): """ Args: ind: Indice de la ligne où se trouve le point le plus proche 'visé'. Returns: Sélectionne la ligne (du tableau) correspondant au point le plus proche 'visé' à la suite de l'événement onpick. """ 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_qtableview_row(self, event): if self.tableView is not None: self.tableView.setFocus() ind = self.indice_points_onpick(event) dataidx_ecran = self.index_pt_plus_proche_ecran(event) self.select_row_pt_clicked(ind[dataidx_ecran]) def select_point_plot(self): """ Returns: sélectionne le(s) point(s) du graphique correspondant à la/aux ligne(s) sélectionnée(s) dans le tableau. """ if self.tableView is not None: rows = list(set( [index.row() for index in self.tableView.selectedIndexes()] )) for row in rows: pass def update_select_point_point(self): if self.tableView is not None: rows = list(set( [index.row() for index in self.tableView.selectedIndexes()] )) if len(rows) > 1: for row in rows: self.pt1 = self.ax.plot(self.x[row], self.y[row], '+', c='Blue', markersize=7) self.pt.append(self.pt1) self.update_select_point_point_bis( self.x[row], self.y[row]) elif len(rows) == 1: for row in rows: try: [pl[0].set_data([], []) for pl in self.pt if len(self.pt) > 1] except Exception: logger.info("update_select_point_point: Update issue") try: self.update_select_point_point_bis(self.x[row], self.y[row]) except Exception: logger.info( "update_select_point_point_bis: Update issue, " + "possible index missing" ) self.ax.figure.canvas.draw_idle() def plot_selec(self): self.point_selec, = self.ax.plot(self.x[0], self.y[0], '+', c='Blue', markersize=7) self.point_selec.set_visible(False) def update_select_point_point_bis(self, x_ind, y_ind): self.point_selec.set_data(x_ind, y_ind) self.point_selec.set_visible(True) self.ax.figure.canvas.draw_idle() def plot_selection_point(self, x, y): """ Args: x: abscissa y: ordinate Returns: sélectionne le point du graphique correspond à la ligne sélectionnée dans le tableau. """ if self.tableView is not None: self.select_point, = self.ax.plot( x, y, '+', c='Blue', markersize=7 ) else: self.select_point, = self.ax.plot([], []) def geometrie_sans_rebord(self): rebord = True z_sans_rebord = [i for i in self.y] x_sans_rebord = [i for i in self.x] while rebord: if z_sans_rebord[1] >= z_sans_rebord[0]: z_sans_rebord.pop(0) x_sans_rebord.pop(0) else: rebord = False rebord = True while rebord: if z_sans_rebord[-1] <= z_sans_rebord[-2]: z_sans_rebord.pop() x_sans_rebord.pop() else: rebord = False z_berge_basse = min(z_sans_rebord[0], z_sans_rebord[-1]) return z_berge_basse, z_sans_rebord, x_sans_rebord @property def z_berge_basse(self): return self.geometrie_sans_rebord()[0] @property def z_sans_rebord(self): return self.geometrie_sans_rebord()[1] @property def x_sans_rebord(self): return self.geometrie_sans_rebord()[2] @property def z_fond(self): return np.array(self.z_sans_rebord) @property def z_point_bas(self): """ Returns: la cote (Zmin) du point le plus bas. """ return min(self.y) @property def delta_x(self): """" Returns: la longueur entre les limites de la vue sur l'axe des x, c'est-à-dire |x_max_visible - x_min_visible|. """ xgauche, xdroite = self.ax.get_xlim() delta_x = abs(xdroite - xgauche) return delta_x @property def delta_y(self): """ Returns: la longueur entre les limites de la vue sur l'axe des y, c'est à dire |y_max_visible - y_min_visible|. """ ybas, yhaut = self.ax.get_ylim() delta_y = abs(yhaut - ybas) return delta_y @staticmethod def indice_points_onpick(event): """ Args: event Returns: le(s) indexe(s) du/des point(s) (plus précisement les coordonnées de points) capturé(s) par l'événement onpick (voir picker) """ return event.ind def points_onpick(self, event): """ Args: event: Returns: une array contenant les coordonées des points qui se trouvent dans la zone définie par l'événement onpick (voir picker) """ thisline = event.artist xdata = thisline.get_xdata() ydata = thisline.get_ydata() points_onpick = np.array( [(xdata[i], ydata[i]) for i in self.indice_points_onpick(event)] ) return points_onpick def distance_normee(self, event): """ Args: event: Returns: la liste des distances normées (en m) entre les points situés dans la région définie par l'événement onpick (voir picker). """ ind = event.ind thisline = event.artist xdata = thisline.get_xdata() ydata = thisline.get_ydata() points_onpick = np.array([(xdata[i], ydata[i]) for i in ind]) distances_normees = [ (((x - event.mouseevent.xdata) / self.delta_x) ** 2 + ((y - event.mouseevent.ydata) / self.delta_y) ** 2) ** (1 / 2) for (x, y) in points_onpick ] return distances_normees def position_souris(self, event): """ Args: event: Returns: la position de la souris """ self.pos_souris = [(event.mouseevent.xdata, event.mouseevent.ydata)] return self.pos_souris def distance_ecran(self, event): """ Args: event: Returns: la liste des distances 'visuelles' entre les points situés dans la région définie par l'événement onpick (voir picker). """ bbox = self.ax.get_window_extent()\ .transformed(self.ax.figure.dpi_scale_trans.inverted()) ratio_w_sur_h = bbox.width / bbox.height distances_ecran = [ ( ( (x - event.mouseevent.xdata) / (self.delta_x * ratio_w_sur_h) ) ** 2 + ( (y - event.mouseevent.ydata) / self.delta_y ) ** 2 ) ** (1 / 2) for (x, y) in self.points_onpick(event) ] return distances_ecran def distances(self, event): """ Args: event: Returns: la liste des distances entre la position de la souris et tous les points se trouvant dans la zone définie par l'événement onpick (voir picker) """ distances = np.linalg.norm( self.points_onpick(event) - self.position_souris(event), axis=1 ) return distances def index_pt_plus_proche_ecran(self, event): """ Args: event: Returns: indice du point le plus proche visuellement de la position du click. """ dataidx_ecran = np.argmin(self.distance_ecran(event)) return dataidx_ecran def point_plus_proche_ecran(self, event): point_onpick = self.points_onpick(event) datapos_ecran = point_onpick[ self.index_pt_plus_proche_ecran(event) ] return self.points_onpick(event)[ self.index_pt_plus_proche_ecran(event) ] def index_pt_plus_proche(self, event): """ Args: event: Returns: indice du point le plus proche de la position du click. """ dataidx = np.argmin(self.distances(event)) return dataidx def point_plus_proche(self, event): """ Args: event: Returns: point le plus proche de la position du click """ point_onpick = self.points_onpick(event) datapos = point_onpick[self.index_pt_plus_proche(event)] return datapos def annotate_onpick(self, x, y): """ Args: x: abscisse du point à annoter. y: ordonnée du point à annoter. Returns: annote le point xy avec du texte text = xytext. """ return self.ax.annotate( "X", xytext=(x, y), xy=(x, y), fontsize=9, bbox=dict( boxstyle='round,pad=0.8', fc='yellow', alpha=0.75 ), arrowprops=dict( arrowstyle='->', connectionstyle='arc3,rad=0.', color='blue' ) ) def on_ylims_change(self, event_ax): return event_ax.get_ylim() def annotate_onclick(self, event): if self.z_point_bas <= event.ydata: self.count += 1 if event.ydata <= self.z_berge_basse: A, p, L = self.calcul_ligne_eau(event.ydata) else: event.ydata = self.z_berge_basse A, p, L = self.calcul_ligne_eau(event.ydata) etiq = f"Z = {event.ydata:.3f} m, A = {A:.3f} "\ f"m\u00B2, p = {p:.3f} m, L = {L:.3f} m" self.annotation_onclick.set_text(etiq) x_min, x_max = self.ax.get_xlim() self.pos_x_annotation = x_min + ((x_max - x_min) / 2) percent = 0 y_ecran_lim = ((max(self.ax.set_ylim()) - min(self.ax.set_ylim())) / 2) if abs(y_ecran_lim) > 4: percent = 0.05 elif 4 < abs(y_ecran_lim) < 1.5: percent = 0.01 elif 0.5 < abs(y_ecran_lim) < 1.5: percent = 0.05 elif 0.25 < abs(y_ecran_lim) < 0.5: percent = 0.08 elif 0 < abs(y_ecran_lim) < 0.25: percent = 0.25 else: percent = 0.1 cte = 0. if abs(event.ydata) < 100: cte = 0.05 else: cte = event.y * 0.1 / 100 self.y_pos_text_param_hydrau = event.ydata + cte self.annotation_onclick.set_position( (self.pos_x_annotation, self.y_pos_text_param_hydrau) ) self.ax.callbacks.connect('ylim_changed', self.on_ylims_change) self.annotation_onclick.set_color("DarkBlue") self.annotation_onclick.set_visible(True) self.annotation_onclick.set_horizontalalignment('center') self.ax.figure.canvas.draw_idle() return self.annotation_onclick def largeur_au_miroir(self, event): if event.ydata <= self.z_berge_basse: self._largeur_miroir.set_data( [min(self.x), max(self.x)], [event.ydata, event.ydata] ) else: self._largeur_miroir.set_data( [min(self.x), max(self.x)], [self.z_berge_basse, self.z_berge_basse] ) return self._largeur_miroir def onpick(self, event): modifiers = QApplication.keyboardModifiers() if modifiers == Qt.ControlModifier: if event.mouseevent.inaxes == self.ax: self.select_qtableview_row(event) x_proche, y_proche = self.point_plus_proche_ecran(event) self.update_select_point_point_bis(x_proche, y_proche) self.ax.figure.canvas.draw_idle() def onclick(self, event): modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == Qt.ShiftModifier: if event.inaxes == self.ax: if self.z_point_bas < event.ydata: try: self.poly_col_bis.remove() self.largeur_au_miroir(event) except Exception: self.largeur_au_miroir(event) self.annotate_onclick(event) self.ax.figure.canvas.draw_idle() def remplir_zone_mouillee(self, x, y1, y2): """ Args: x: Les coordonnées x des nœuds définissant la courbe. y1: points définisant le polygone à déssiner. y2: points définisant le polygone à déssiner. Returns: dessine et colorie la région définie par le polygone. """ return self.ax.fill_between( x, y1=y1, y2=y2, where=y1 > y2, interpolate=True, facecolor='skyblue', alpha=0.7 ) def calcul_ligne_eau(self, val: float) -> (float, float, float): """ Args: val: Valeur de la cote Z à laquelle on veut caluler A , p et L. Returns: la valeur de la section mouillée A, du périmètre mouillé p et de la largeur au miroir L. """ largeur_miroir = 0. section_mouillee_totale = 0. perimetre_mouille_total = 0. if self.z_point_bas < val <= self.z_berge_basse: z_eau = np.array([val] * (len(self.z_sans_rebord))) self.poly_col_bis = self.remplir_zone_mouillee(self.x_sans_rebord, z_eau, self.z_sans_rebord) liste_chemins = self.poly_col_bis.get_paths() couleurs = ['crimson', 'pink'] * len(liste_chemins) aire_calculee_shapely = None perimetre_mouille_total_shapely = None perimetre_shapely = 0. perim_calc = 0. for polyg, coul in zip( liste_chemins, couleurs[0:len(liste_chemins)] ): points_polygone = polyg.vertices xs = points_polygone[:, 0] ys = points_polygone[:, 1] liste_points_miroir = [ x for (x, y) in zip(xs, ys) if np.isclose(y, val) ] largeur_miroir_polygone = liste_points_miroir[-2] - \ liste_points_miroir[0] largeur_miroir += largeur_miroir_polygone polygone_shapely = ShapelyPolygon(points_polygone) aire_calculee_shapely = polygone_shapely.area perimetre_shapely = polygone_shapely.length perimetre_mouille_total_shapely = ( polygone_shapely.length - largeur_miroir ) liste_points_fond = [ (x, y) for (x, y) in zip(xs, ys) if not np.isclose(y, val) ] x_pt_prec, y_pt_prec = max(liste_points_miroir), val perimetre = 0 aire = 0 for un_point in liste_points_fond + [ (min(liste_points_miroir), val) ]: x_pt_suivant, y_pt_suivant = un_point perimetre += ((x_pt_prec - x_pt_suivant) ** 2 + (y_pt_prec - y_pt_suivant) ** 2) ** (1 / 2) aire += (((val - y_pt_prec) + (val - y_pt_suivant)) * abs(x_pt_suivant - x_pt_prec) / 2) x_pt_prec, y_pt_prec = x_pt_suivant, y_pt_suivant perim_calc = perimetre perimetre_mouille_total = perimetre_shapely - largeur_miroir section_mouillee_totale = aire_calculee_shapely return section_mouillee_totale, perimetre_mouille_total, largeur_miroir