def mouseEventCallback(self, callback): if self.cid != None: FigureCanvas.mpl_disconnect(self, self.cid) self.cid = FigureCanvas.mpl_connect(self, 'button_press_event', callback)
class PlotCanvas(QtCore.QObject): """ Class handling the plotting area in the application. """ # Signals: # Request for new bitmap to display. The parameter # is a list with [xmin, xmax, ymin, ymax, zoom(optional)] update_screen_request = QtCore.pyqtSignal(list) def __init__(self, container, app): """ The constructor configures the Matplotlib figure that will contain all plots, creates the base axes and connects events to the plotting area. :param container: The parent container in which to draw plots. :rtype: PlotCanvas """ super(PlotCanvas, self).__init__() self.app = app # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels # Parent container self.container = container # Plots go onto a single matplotlib.figure self.figure = Figure(dpi=50) # TODO: dpi needed? self.figure.patch.set_visible(False) # These axes show the ticks and grid. No plotting done here. # New axes must have a label, otherwise mpl returns an existing one. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) self.axes.set_aspect(1) self.axes.grid(True) self.axes.axhline(color='Black') self.axes.axvline(color='Black') # The canvas is the top level container (FigureCanvasQTAgg) self.canvas = FigureCanvas(self.figure) # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # self.canvas.setFocus() #self.canvas.set_hexpand(1) #self.canvas.set_vexpand(1) #self.canvas.set_can_focus(True) # For key press # Attach to parent #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? self.container.addWidget(self.canvas) # Qt # Copy a bitmap of the canvas for quick animation. # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) ### Bitmap Cache self.cache = CanvasCache(self, self.app) self.cache_thread = QtCore.QThread() self.cache.moveToThread(self.cache_thread) #super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run) # self.connect() self.cache_thread.start() self.cache.new_screen.connect(self.on_new_screen) # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) self.canvas.mpl_connect('button_release_event', self.on_mouse_release) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) #self.canvas.connect('configure-event', self.auto_adjust_axes) self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) #self.canvas.connect("scroll-event", self.on_scroll) self.canvas.mpl_connect('scroll_event', self.on_scroll) self.canvas.mpl_connect('key_press_event', self.on_key_down) self.canvas.mpl_connect('key_release_event', self.on_key_up) self.canvas.mpl_connect('draw_event', self.on_draw) self.mouse = [0, 0] self.key = None self.pan_axes = [] self.panning = False def on_new_screen(self): log.debug("Cache updated the screen!") def on_key_down(self, event): """ :param event: :return: """ FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key)) self.key = event.key def on_key_up(self, event): """ :param event: :return: """ self.key = None def mpl_connect(self, event_name, callback): """ Attach an event handler to the canvas through the Matplotlib interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: func :return: Connection id :rtype: int """ return self.canvas.mpl_connect(event_name, callback) def mpl_disconnect(self, cid): """ Disconnect callback with the give id. :param cid: Callback id. :return: None """ self.canvas.mpl_disconnect(cid) def connect(self, event_name, callback): """ Attach an event handler to the canvas through the native Qt interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: function :return: Nothing """ self.canvas.connect(event_name, callback) def clear(self): """ Clears axes and figure. :return: None """ # Clear self.axes.cla() try: self.figure.clf() except KeyError: FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") # Re-build self.figure.add_axes(self.axes) self.axes.set_aspect(1) self.axes.grid(True) # Re-draw self.canvas.draw_idle() def adjust_axes(self, xmin, ymin, xmax, ymax): """ Adjusts all axes while maintaining the use of the whole canvas and an aspect ratio to 1:1 between x and y axes. The parameters are an original request that will be modified to fit these restrictions. :param xmin: Requested minimum value for the X axis. :type xmin: float :param ymin: Requested minimum value for the Y axis. :type ymin: float :param xmax: Requested maximum value for the X axis. :type xmax: float :param ymax: Requested maximum value for the Y axis. :type ymax: float :return: None """ # FlatCAMApp.App.log.debug("PC.adjust_axes()") width = xmax - xmin height = ymax - ymin try: r = width / height except ZeroDivisionError: FlatCAMApp.App.log.error("Height is %f" % height) return canvas_w, canvas_h = self.canvas.get_width_height() canvas_r = float(canvas_w) / canvas_h x_ratio = float(self.x_margin) / canvas_w y_ratio = float(self.y_margin) / canvas_h if r > canvas_r: ycenter = (ymin + ymax) / 2.0 newheight = height * r / canvas_r ymin = ycenter - newheight / 2.0 ymax = ycenter + newheight / 2.0 else: xcenter = (xmax + xmin) / 2.0 newwidth = width * canvas_r / r xmin = xcenter - newwidth / 2.0 xmax = xcenter + newwidth / 2.0 # Adjust axes for ax in self.figure.get_axes(): if ax._label != 'base': ax.set_frame_on(False) # No frame ax.set_xticks([]) # No tick ax.set_yticks([]) # No ticks ax.patch.set_visible(False) # No background ax.set_aspect(1) ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) # Sync re-draw to proper paint on form resize self.canvas.draw() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def auto_adjust_axes(self, *args): """ Calls ``adjust_axes()`` using the extents of the base axes. :rtype : None :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() self.adjust_axes(xmin, ymin, xmax, ymax) def zoom(self, factor, center=None): """ Zooms the plot by factor around a given center point. Takes care of re-drawing. :param factor: Number by which to scale the plot. :type factor: float :param center: Coordinates [x, y] of the point around which to scale the plot. :type center: list :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin if center is None or center == [None, None]: center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] # For keeping the point at the pointer location relx = (xmax - center[0]) / width rely = (ymax - center[1]) / height new_width = width / factor new_height = height / factor xmin = center[0] - new_width * (1 - relx) xmax = center[0] + new_width * relx ymin = center[1] - new_height * (1 - rely) ymax = center[1] + new_height * rely # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) # Async re-draw self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def pan(self, x, y): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin + x * width, xmax + x * width)) ax.set_ylim((ymin + y * height, ymax + y * height)) # Re-draw self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def new_axes(self, name): """ Creates and returns an Axes object attached to this object's Figure. :param name: Unique label for the axes. :return: Axes attached to the figure. :rtype: Axes """ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) def on_scroll(self, event): """ Scroll event handler. :param event: Event object containing the event information. :return: None """ # So it can receive key presses # self.canvas.grab_focus() self.canvas.setFocus() # Event info # z, direction = event.get_scroll_direction() if self.key is None: if event.button == 'up': self.zoom(1.5, self.mouse) else: self.zoom(1 / 1.5, self.mouse) return if self.key == 'shift': if event.button == 'up': self.pan(0.3, 0) else: self.pan(-0.3, 0) return if self.key == 'control': if event.button == 'up': self.pan(0, 0.3) else: self.pan(0, -0.3) return def on_mouse_press(self, event): # Check for middle mouse button press if event.button == self.app.mouse_pan_button: # Prepare axes for pan (using 'matplotlib' pan function) self.pan_axes = [] for a in self.figure.get_axes(): if (event.x is not None and event.y is not None and a.in_axes(event) and a.get_navigate() and a.can_pan()): a.start_pan(event.x, event.y, 1) self.pan_axes.append(a) # Set pan view flag if len(self.pan_axes) > 0: self.panning = True; def on_mouse_release(self, event): # Check for middle mouse button release to complete pan procedure if event.button == self.app.mouse_pan_button: for a in self.pan_axes: a.end_pan() # Clear pan flag self.panning = False def on_mouse_move(self, event): """ Mouse movement event hadler. Stores the coordinates. Updates view on pan. :param event: Contains information about the event. :return: None """ self.mouse = [event.xdata, event.ydata] # Update pan view on mouse move if self.panning is True: for a in self.pan_axes: a.drag_pan(1, event.key, event.x, event.y) # Async re-draw (redraws only on thread idle state, uses timer on backend) self.canvas.draw_idle() ##### Temporary place-holder for cached update ##### self.update_screen_request.emit([0, 0, 0, 0, 0]) def on_draw(self, renderer): # Store background on canvas redraw self.background = self.canvas.copy_from_bbox(self.axes.bbox) def get_axes_pixelsize(self): """ Axes size in pixels. :return: Pixel width and height :rtype: tuple """ bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) width, height = bbox.width, bbox.height width *= self.figure.dpi height *= self.figure.dpi return width, height def get_density(self): """ Returns unit length per pixel on horizontal and vertical axes. :return: X and Y density :rtype: tuple """ xpx, ypx = self.get_axes_pixelsize() xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin return width / xpx, height / ypx
class MainWindow(QtGui.QMainWindow, OnClick, OnMotion): def __init__(self): super(MainWindow, self).__init__() uic.loadUi("PeakInspector_layout.ui", self) self.setWindowTitle("PeakInspector (beta) (c) A.Salykin - Masaryk University - CC-BY-SA 4.0") # main variable: self.multiple_data_sets = pd.DataFrame() # Initialise the final dataframe to export to Excel self.coordinates = [] self.area = [] self.amplitudes = [] self.amplitude_line_coordinates = [] self.left_peak_border = [] self.right_peak_border = [] self.pickable_artists_pts_AX2 = [] self.pickable_artists_pts_AX3 = [] self.pickable_artists_lns_AX3 = [] self.pickable_artists_fill_AX3 = [] self.pickable_artists_plb_AX3 = [] self.pickable_artists_prb_AX3 = [] self.pickable_artists_lnsP_AX3 = [] self.left_border = [] self.right_border = [] # Connect buttons to class methods: self.BtnLoadFile.clicked.connect(self.load_file) self.BtnReplot.clicked.connect(self.replot_graph) self.chbxDotPickEnable.stateChanged.connect(self.dot_pick_enable) self.BtnSaveCurrent.clicked.connect(self.coordinates_analysis) self.BtnSaveFullDataset.clicked.connect(self.save_data) self.BoxMplPlotStyle.currentIndexChanged.connect(self.mpl_style_change) style.use(self.BoxMplPlotStyle.currentText()) self.BtnLoadFile.setStyleSheet("background-color: #7CF2BD") self.BtnReplot.setStyleSheet("background-color: #FAF6F2") self.BtnSaveCurrent.setStyleSheet("background-color: #FAF6F2") self.BtnSaveFullDataset.setStyleSheet("background-color: #FAF6F2") # Initialise figure instance self.fig = plt.figure() self.show() def addmpl(self, ): self.canvas = FigureCanvas(self.fig) self.toolbar = NavigationToolbar(self.canvas, self.CanvasWidget, coordinates=True) self.CanvasLayout.addWidget(self.toolbar) self.CanvasLayout.addWidget(self.canvas) if self.chbxDotPickEnable.isChecked(): self.cid_click = self.canvas.mpl_connect('button_press_event', self.on_click) self.cid_motion = self.canvas.mpl_connect('motion_notify_event', self.on_motion) self.canvas.draw() def rmmpl(self, ): # self.canvas.mpl_disconnect(self.cid_click) self.canvas.mpl_disconnect(self.cid_motion) self.CanvasLayout.removeWidget(self.canvas) self.canvas.close() self.CanvasLayout.removeWidget(self.toolbar) self.toolbar.close() def dot_pick_enable(self, ): # if checked, user can choose peaks try: # if figure and canvas is initiated if self.chbxDotPickEnable.isChecked(): self.cid_click = self.canvas.mpl_connect('button_press_event', self.on_click) self.cid_motion = self.canvas.mpl_connect('motion_notify_event', self.on_motion) else: self.canvas.mpl_disconnect(self.cid_click) self.canvas.mpl_disconnect(self.cid_motion) except: message = MessageBox() message.about(self, 'Warning!', "File was not loaded! \n Please be sure that your file has \ \n 1) 1 or 2 columns; \n 2) check headers, footers and delimeter \n and try again.") def load_file(self, ): self.BtnLoadFile.setStyleSheet("background-color: #FAF6F2") # Check if we already have some file loaded - then remove canvas if hasattr(self, 'cid_click'): self.rmmpl() # Make sure that np data arrays and lists from previous dataset are empty self.x = np.empty([]) self.y = np.empty([]) self.clear_data() name = QtGui.QFileDialog.getOpenFileName(self, 'Open File') if not name: return self.import_error() # get more readable file name for graph title try: slash_index = self.find_character(name, '/') dot_index = self.find_character(name, '.') self.graph_name = name[slash_index[-1] + 1:dot_index[-1]] except: self.graph_name = name[-10:] skip_header_rows = self.BoxSkipHeader.value() skip_footer_rows = self.BoxSkipFooter.value() if self.BoxDelimeterChoice.currentText() == 'Tab': delimiter = "\t" elif self.BoxDelimeterChoice.currentText() == 'Space': delimiter = " " elif self.BoxDelimeterChoice.currentText() == 'Comma': delimiter = "," elif self.BoxDelimeterChoice.currentText() == 'Dot': delimiter = "." # unpack file try: # if data file has 2 columns self.x, self.y = np.genfromtxt(name, delimiter = delimiter, skip_header = skip_header_rows, skip_footer = skip_footer_rows, unpack = True) if len(self.y) < 100: return self.import_error() return self.process_opened_file() except: # if data file has 1 column self.y = np.genfromtxt(name, skip_header=skip_header_rows, skip_footer=skip_footer_rows, unpack=True) if len(self.y) < 100: return self.import_error() self.x = np.arange(0, len(self.y), 1) return self.process_opened_file() def import_error(self,): message = MessageBox() message.about(self, 'Warning!', "Data were not loaded. \n Please, be sure that:\n " "1. Data have 1 or 2 columns.\n" "2. Data are longer than 100 points.\n" "3. Delimiter is correctly specified.\n" "4. Rows in data contain only numeric values\n") def process_opened_file(self, ): self.x = tuple(self.x) self.data_preprocessing(self.y) self.baseline_calculation() self.plot_data() def data_preprocessing(self, data_to_preprocess): try: # Detrend dataset if self.chbxDetrendData.isChecked(): self.data_detrended = sig.detrend(data_to_preprocess) else: self.data_detrended = data_to_preprocess # Application of Savitzkyi-Golay filter for data smoothing sg_window_frame = self.BoxSGwindowFrame.value() sg_polynom_degree = self.BoxSGpolynomDegree.value() self.data_after_filter = sig.savgol_filter(self.data_detrended, sg_window_frame, sg_polynom_degree) except: message = MessageBox() message.about(self, 'Warning!', "Not possible to detrend and/or smooth data! \n Please check your dataset and try again.") def baseline_calculation(self, ): ''' Calculate baseline of detrended data and add it to dataset for baseline to be equal 0 ''' databaseline = min(self.data_after_filter) if self.chbxDetrendData.isChecked(): self.data_after_filter = [i + abs(databaseline) for i in self.data_after_filter] self.data_detrended = [i + abs(databaseline) for i in self.data_detrended] else: self.data_after_filter = [i - abs(databaseline) for i in self.data_after_filter] self.data_detrended = [i - abs(databaseline) for i in self.data_detrended] def interpolation(self, p1, p2, left_index, right_index): f = interpolate.interp1d([p1[0], p2[0]], [p1[1], p2[1]]) num = len(self.x[left_index:right_index]) xx = np.linspace(self.x[left_index], self.x[right_index], num) return f(xx) def plot_data(self, ): if self.BoxPlotCustomStyle.currentText() == 'Line': plot_style_custom = '-' marker_size = 1 elif self.BoxPlotCustomStyle.currentText() == 'Line & small markers': plot_style_custom = 'o-' marker_size = 3 elif self.BoxPlotCustomStyle.currentText() == 'Line & big markers': plot_style_custom = 'o-' marker_size = 6 elif self.BoxPlotCustomStyle.currentText() == 'Small markers': plot_style_custom = 'o' marker_size = 3 elif self.BoxPlotCustomStyle.currentText() == 'Big markers': plot_style_custom = 'o' marker_size = 6 font_size = 14 self.ax1 = plt.subplot2grid((4, 1), (0, 0), rowspan=1, colspan=1) plt.title(self.graph_name) self.ax1.plot(self.x, self.y, plot_style_custom, ms=marker_size, linewidth=1) # plot raw data plt.ylabel('Original raw data', fontsize=font_size) self.ax2 = plt.subplot2grid((4, 1), (1, 0), rowspan=1, colspan=1) self.ax2.plot(self.x, self.data_detrended, plot_style_custom, ms=marker_size, linewidth=1) # plot detrended data plt.ylabel('Detrended data', fontsize=font_size) self.ax3 = plt.subplot2grid((4, 1), (2, 0), rowspan=2, colspan=1, sharex=self.ax2, sharey=self.ax2) self.ax3.plot(self.x, self.data_after_filter, plot_style_custom, ms=marker_size, linewidth=1) # plot filtered detrended data self.baselinePlotArtist = self.ax3.plot([self.x[0], self.x[-1]], [0, 0], 'k', linewidth=1) # plot baseline plt.ylabel('Savitzky-Golay filter \n for detrended data', fontsize=font_size) self.ax3.set_xlim(0, self.x[-1]) plt.xlabel('Time, sec') self.addmpl() def replot_graph(self, ): self.clear_data() self.rmmpl() self.data_preprocessing(self.y) self.baseline_calculation() self.plot_data() def coordinates_analysis(self, ): """ Main function """ coord_x, coord_y = zip(*self.coordinates) leftpb_x, leftpb_y = zip(*self.left_peak_border) rightpb_x, rightpb_y= zip(*self.right_peak_border) # absolute amplitude % and MAX relative_amplitude = [] ampl_max = max(self.amplitudes) relative_amplitude[:] = [(i / ampl_max) for i in self.amplitudes] # create temporal Pandas DataFrame for sorting and calculation: temp_dataset = list( zip(coord_x, self.amplitudes, relative_amplitude, leftpb_x, leftpb_y, rightpb_x, rightpb_y, self.area)) df = pd.DataFrame(data=temp_dataset, columns=['Peak Time', 'Amplitude', 'Relative Amplitude \n (F/Fmax)', 'Peak Start Time', 'Peak Start Ordinate', 'Peak Stop Time', 'Peak Stop Ordinate', 'Area']) # Sort data in DataFrame according to the time of peak appearance df_sorted = df.sort_values(['Peak Time'], ascending=True) df_sorted.index = range(0, len(df_sorted)) # reset indexing # calculate periods periods = [] for i in range(1, len(df_sorted['Peak Time'])): periods.append(df_sorted.at[i, 'Peak Time'] - df_sorted.at[i - 1, 'Peak Time']) periods.insert(0, np.nan) # add placeholder because len(periods)=len(peaks)-1 # calculate frequencies based on calculated periods frequencies = [] frequencies[:] = [(1 / i) for i in periods] # Analise peak start - stop time (left and right peak borders) peak_full_time = [] for i in range(0, len(df_sorted['Peak Time']), 1): peak_full_time.append(df_sorted.at[i, 'Peak Stop Time'] - df_sorted.at[i, 'Peak Start Time']) peak_up_time = [] for i in range(0, len(df_sorted['Peak Time']), 1): peak_up_time.append(df_sorted.at[i, 'Peak Time'] - df_sorted.at[i, 'Peak Start Time']) peak_down_time = [] for i in range(0, len(df_sorted['Peak Time']), 1): peak_down_time.append(df_sorted.at[i, 'Peak Stop Time'] - df_sorted.at[i, 'Peak Time']) # Compute area under the peak using the composite trapezoidal rule. peak_area = [] for i in range(0, len(df_sorted['Peak Time']), 1): peak_area.append(np.trapz(df_sorted.at[i, 'Area'])) # Analise the peak decay area half_decay_time = [] half_decay_amplitude = [] for i in range(0, len(df_sorted['Peak Time']), 1): half_decay_ampl = df_sorted.at[i, 'Amplitude'] / 2 # calculate the half of the amplitude peak_index = self.x.index(df_sorted.at[i, 'Peak Time']) # find index of the peak time stop_idx = self.x.index(df_sorted.at[i, 'Peak Stop Time']) # find index of the right peak border data_decay_region = self.data_after_filter[peak_index:stop_idx] # determine the amplitude region where to search for halftime decay index time_decay_region = self.x[peak_index:stop_idx] half_decay_idx = (np.abs(data_decay_region - half_decay_ampl)).argmin() # find the closet value in data_decay_region that corresponds to the half amplitude half_decay_amplitude.append(half_decay_ampl) half_decay_time.append(time_decay_region[half_decay_idx] - df_sorted.at[i, 'Peak Time']) # Compute amplitude normalised to the baseline normalised_amplitude = [] sg_window_frame = self.BoxSGwindowFrame.value() sg_polynom_degree = self.BoxSGpolynomDegree.value() orig_data_filtered = sig.savgol_filter(self.y, sg_window_frame, sg_polynom_degree) for i in range(0, len(df_sorted['Peak Time']), 1): start_idx = self.x.index(df_sorted.at[i, 'Peak Start Time']) F0 = orig_data_filtered[start_idx] amplitude_normed_computation = df_sorted.at[i, 'Amplitude'] / F0 normalised_amplitude.append(amplitude_normed_computation) # normalised amplitude % relative_normalised_amplitude = [] maxATB = max(normalised_amplitude) relative_normalised_amplitude[:] = [(i / maxATB) for i in normalised_amplitude] # normalised amplitude MAX normalised_amplitude_max = list(range(0, len(df_sorted['Peak Time']) - 1)) normalised_amplitude_max[:] = [np.nan for _ in normalised_amplitude_max] normalised_amplitude_max.insert(0, maxATB) # add file name as first column file_name = list(range(0, len(df_sorted['Peak Time']) - 1)) file_name[:] = [np.nan for _ in file_name] file_name.insert(0, self.graph_name) # add maximum amplitude absolute_amplitude_max = list(range(0, len(df_sorted['Peak Time']) - 1)) absolute_amplitude_max[:] = [np.nan for _ in absolute_amplitude_max] absolute_amplitude_max.insert(0, max(df_sorted['Amplitude'])) # peak sorting big_peaks_number = [p for p in self.amplitudes if (p > ampl_max * 0.66)] medium_peaks_number = [p for p in self.amplitudes if (p > ampl_max * 0.33 and p <= ampl_max * 0.66)] small_peaks_number = [p for p in self.amplitudes if (p > 0 and p <= ampl_max * 0.33)] big_peaks_frequency = list(range(0, len(df_sorted['Peak Time']) - 1)) big_peaks_frequency[:] = [np.nan for _ in big_peaks_frequency] big_peaks_frequency.insert(0, len(big_peaks_number) / (self.x[-1] - self.x[0])) medium_peaks_frequency = list(range(0, len(df_sorted['Peak Time']) - 1)) medium_peaks_frequency[:] = [np.nan for _ in medium_peaks_frequency] medium_peaks_frequency.insert(0, len(medium_peaks_number) / (self.x[-1] - self.x[0])) small_peaks_frequency = list(range(0, len(df_sorted['Peak Time']) - 1)) small_peaks_frequency[:] = [np.nan for _ in small_peaks_frequency] small_peaks_frequency.insert(0, len(small_peaks_number) / (self.x[-1] - self.x[0])) final_dataset = list(zip(file_name, df_sorted['Peak Time'], df_sorted['Amplitude'], df_sorted['Relative Amplitude \n (F/Fmax)'], absolute_amplitude_max, normalised_amplitude, relative_normalised_amplitude, normalised_amplitude_max, periods, frequencies, half_decay_time, half_decay_amplitude, df_sorted['Peak Start Time'], df_sorted['Peak Start Ordinate'], df_sorted['Peak Stop Time'], df_sorted['Peak Stop Ordinate'], peak_up_time, peak_down_time, peak_full_time, peak_area, big_peaks_frequency, medium_peaks_frequency, small_peaks_frequency)) final_dataframe = pd.DataFrame(data=final_dataset, columns=['File name', 'Peak time', 'Absolute amplitude', 'Absolute amplitude (%)', 'Absolute amplitude MAX', 'Normalised amplitude', 'Normalised amplitude (%)', 'Normalised amplitude MAX', 'Period', 'Frequency', 'Half-decay time', 'Half-decay amplitude', 'Start time', 'Start ordinate', 'Stop time', 'Stop ordinate', 'Ascending time', 'Decay time', 'Full peak time', 'AUC', 'Big peaks, Hz', 'Mid peaks, Hz', 'Small peaks, Hz']) # specify data for export acording to the settings tab in GUI # and append current analysed dataset to existing ones try: columns_to_delete_for_export = [] if not self.chbxFileName.isChecked(): columns_to_delete_for_export.append('File name') if not self.chbxPeakTime.isChecked(): columns_to_delete_for_export.append('Peak time') if not self.chbxAmplAbs.isChecked(): columns_to_delete_for_export.append('Absolute amplitude') if not self.chbxAmplAbsRel.isChecked(): columns_to_delete_for_export.append('Absolute amplitude (%)') if not self.chbxAmplAbsMax.isChecked(): columns_to_delete_for_export.append('Absolute amplitude MAX') if not self.chbxAmplNorm.isChecked(): columns_to_delete_for_export.append('Normalised amplitude') if not self.chbxAmplNormRel.isChecked(): columns_to_delete_for_export.append('Normalised amplitude (%)') if not self.chbxAmplNormMax.isChecked(): columns_to_delete_for_export.append('Normalised amplitude MAX') if not self.chbxPeriod.isChecked(): columns_to_delete_for_export.append('Period') if not self.chbxFreq.isChecked(): columns_to_delete_for_export.append('Frequency') if not self.chbxHalfDecayTime.isChecked(): columns_to_delete_for_export.append('Half-decay time') if not self.chbxHalfDecayAmpl.isChecked(): columns_to_delete_for_export.append('Half-decay amplitude') if not self.chbxLeftBorderTime.isChecked(): columns_to_delete_for_export.append('Start time') if not self.chbxLeftBorder.isChecked(): columns_to_delete_for_export.append('Start ordinate') if not self.chbxRightBorderTime.isChecked(): columns_to_delete_for_export.append('Stop time') if not self.chbxRightBorder.isChecked(): columns_to_delete_for_export.append('Stop ordinate') if not self.chbxTimeToPeak.isChecked(): columns_to_delete_for_export.append('Ascending time') if not self.chbxDecayTime.isChecked(): columns_to_delete_for_export.append('Decay time') if not self.chbxFullPeakTime.isChecked(): columns_to_delete_for_export.append('Full peak time') if not self.chbxAUC.isChecked(): columns_to_delete_for_export.append('AUC') if not self.chbxSmallPeaks.isChecked(): columns_to_delete_for_export.append('Big peaks, Hz') if not self.chbxMidPeaks.isChecked(): columns_to_delete_for_export.append('Mid peaks, Hz') if not self.chbxBigPeaks.isChecked(): columns_to_delete_for_export.append('Small peaks, Hz') final_dataframe.drop(columns_to_delete_for_export, axis=1, inplace=True) self.multiple_data_sets = self.multiple_data_sets.append(final_dataframe) if self.chbxSaveFig.isChecked(): os.makedirs('_Figures', exist_ok=True) dpi = self.BoxDPI.value() plt.savefig(os.path.join('_Figures', 'Fig_{figName}.png'.format(figName=self.graph_name)), dpi=dpi) del df del df_sorted del final_dataframe dialog = MessageBox.question(self, '', "Current dataset was analysed \n and added to previous ones (if exist). \n Would you like to load next file? ", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if dialog == QtGui.QMessageBox.Yes: self.load_file() else: self.rmmpl() self.BtnSaveFullDataset.setStyleSheet("background-color: #7CF2BD") self.BtnLoadFile.setStyleSheet("background-color: #7CF2BD") except: message = MessageBox() message.about(self, 'Warning!', "Data were not added to existing dataset. \n Plese be sure that you did not change the output settings.") def save_data(self, ): try: file_name = QtGui.QFileDialog.getSaveFileName(self, 'Save file') writer = pd.ExcelWriter('{}.xlsx'.format(file_name)) self.multiple_data_sets.to_excel(writer, index=True, sheet_name='Results') writer.sheets['Results'].set_zoom(80) writer.sheets['Results'].set_column('A:A', 5) writer.sheets['Results'].set_column('B:X', 23) writer.save() message = MessageBox() message.about(self, 'Data saved', "Data were saved!") self.multiple_data_sets = pd.DataFrame() self.BtnSaveFullDataset.setStyleSheet("background-color: #FAF6F2") self.BtnLoadFile.setStyleSheet("background-color: #7CF2BD") except: message = MessageBox() message.about(self, 'Warning!', "Data were not exported to Excel! \n Please try again.") def mpl_style_change(self, ): style.use(self.BoxMplPlotStyle.currentText()) def clear_data(self): self.coordinates = [] self.area = [] self.amplitudes = [] self.amplitude_line_coordinates = [] self.left_peak_border = [] self.right_peak_border = [] self.pickable_artists_pts_AX2 = [] self.pickable_artists_pts_AX3 = [] self.pickable_artists_lns_AX3 = [] self.pickable_artists_fill_AX3 = [] self.pickable_artists_plb_AX3 = [] self.pickable_artists_prb_AX3 = [] self.pickable_artists_lnsP_AX3 = [] def closeEvent(self, event): """Exchange default event to add a dialog""" if self.multiple_data_sets.empty: reply = MessageBox.question(self, 'Warning!', "Are you sure to quit?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) else: reply = MessageBox.question(self, 'Warning!', "You have unsaved analysed data! \n Are you sure to quit?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: event.accept() else: event.ignore() @staticmethod def find_character(s, ch): # for graph title return [i for i, ltr in enumerate(s) if ltr == ch]
class ProfileDockWidget(QDockWidget): """ DockWidget class to display the profile """ closeSignal = pyqtSignal() def __init__(self, iface): """ Constructor :param iface: interface """ QDockWidget.__init__(self) self.setWindowTitle(QCoreApplication.translate("VDLTools", "Profile Tool")) self.resize(1024, 400) self.__iface = iface self.__canvas = self.__iface.mapCanvas() self.__types = ['PDF', 'PNG'] # ], 'SVG', 'PS'] self.__libs = [] if Qwt5_loaded: self.__lib = 'Qwt5' self.__libs.append('Qwt5') if matplotlib_loaded: self.__libs.append('Matplotlib') elif matplotlib_loaded: self.__lib = 'Matplotlib' self.__libs.append('Matplotlib') else: self.__lib = None self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "No graph lib available (qwt5 or matplotlib)"), level=QgsMessageBar.CRITICAL, duration=0) self.__doTracking = False self.__vline = None self.__profiles = None self.__numLines = None self.__mntPoints = None self.__marker = None self.__tabmouseevent = None self.__contentWidget = QWidget() self.setWidget(self.__contentWidget) self.__boxLayout = QHBoxLayout() self.__contentWidget.setLayout(self.__boxLayout) self.__plotFrame = QFrame() self.__frameLayout = QHBoxLayout() self.__plotFrame.setLayout(self.__frameLayout) self.__printLayout = QHBoxLayout() self.__printLayout.addWidget(self.__plotFrame) self.__legendLayout = QVBoxLayout() self.__printLayout.addLayout(self.__legendLayout) self.__printWdg = QWidget() self.__printWdg.setLayout(self.__printLayout) self.__plotWdg = None self.__changePlotWidget() size = QSize(150, 20) self.__boxLayout.addWidget(self.__printWdg) self.__vertLayout = QVBoxLayout() self.__libCombo = QComboBox() self.__libCombo.setFixedSize(size) self.__libCombo.addItems(self.__libs) self.__vertLayout.addWidget(self.__libCombo) self.__libCombo.currentIndexChanged.connect(self.__setLib) self.__maxLabel = QLabel("y max") self.__maxLabel.setFixedSize(size) self.__vertLayout.addWidget(self.__maxLabel) self.__maxSpin = QSpinBox() self.__maxSpin.setFixedSize(size) self.__maxSpin.setRange(-10000, 10000) self.__maxSpin.valueChanged.connect(self.__reScalePlot) self.__vertLayout.addWidget(self.__maxSpin) self.__vertLayout.insertSpacing(10, 20) self.__minLabel = QLabel("y min") self.__minLabel.setFixedSize(size) self.__vertLayout.addWidget(self.__minLabel) self.__minSpin = QSpinBox() self.__minSpin.setFixedSize(size) self.__minSpin.setRange(-10000, 10000) self.__minSpin.valueChanged.connect(self.__reScalePlot) self.__vertLayout.addWidget(self.__minSpin) self.__vertLayout.insertSpacing(10, 40) self.__typeCombo = QComboBox() self.__typeCombo.setFixedSize(size) self.__typeCombo.addItems(self.__types) self.__vertLayout.addWidget(self.__typeCombo) self.__saveButton = QPushButton(QCoreApplication.translate("VDLTools", "Save")) self.__saveButton.setFixedSize(size) self.__saveButton.clicked.connect(self.__save) self.__vertLayout.addWidget(self.__saveButton) self.__boxLayout.addLayout(self.__vertLayout) self.__maxSpin.setEnabled(False) self.__minSpin.setEnabled(False) self.__colors = [] for cn in QColor.colorNames(): qc = QColor(cn) val = qc.red() + qc.green() + qc.blue() if 0 < val < 450: self.__colors.append(cn) def __changePlotWidget(self): """ When plot widget is change (qwt <-> matplotlib) """ self.__activateMouseTracking(False) while self.__frameLayout.count(): child = self.__frameLayout.takeAt(0) child.widget().deleteLater() self.__plotWdg = None if self.__lib == 'Qwt5': self.__plotWdg = QwtPlot(self.__plotFrame) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(10) sizePolicy.setVerticalStretch(0) self.__plotWdg.setSizePolicy(sizePolicy) self.__plotWdg.setAutoFillBackground(False) # Decoration self.__plotWdg.setCanvasBackground(Qt.white) self.__plotWdg.plotLayout().setAlignCanvasToScales(False) self.__plotWdg.plotLayout().setSpacing(100) self.__plotWdg.plotLayout().setCanvasMargin(10, QwtPlot.xBottom) self.__plotWdg.plotLayout().setCanvasMargin(10, QwtPlot.yLeft) title = QwtText(QCoreApplication.translate("VDLTools", "Distance [m]")) title.setFont(QFont("Helvetica", 10)) self.__plotWdg.setAxisTitle(QwtPlot.xBottom, title) title.setText(QCoreApplication.translate("VDLTools", "Elevation [m]")) title.setFont(QFont("Helvetica", 10)) self.__plotWdg.setAxisTitle(QwtPlot.yLeft, title) self.__zoomer = QwtPlotZoomer(QwtPlot.xBottom, QwtPlot.yLeft, QwtPicker.DragSelection, QwtPicker.AlwaysOff, self.__plotWdg.canvas()) self.__zoomer.setRubberBandPen(QPen(Qt.blue)) grid = QwtPlotGrid() grid.setPen(QPen(QColor('grey'), 0, Qt.DotLine)) grid.attach(self.__plotWdg) self.__frameLayout.addWidget(self.__plotWdg) elif self.__lib == 'Matplotlib': # __plotWdg.figure : matplotlib.figure.Figure fig = Figure((1.0, 1.0), linewidth=0.0, subplotpars=SubplotParams(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)) font = {'family': 'arial', 'weight': 'normal', 'size': 12} rc('font', **font) rect = fig.patch rect.set_facecolor((0.9, 0.9, 0.9)) self.__axes = fig.add_axes((0.07, 0.16, 0.92, 0.82)) self.__axes.set_xbound(0, 1000) self.__axes.set_ybound(0, 1000) self.__manageMatplotlibAxe(self.__axes) self.__plotWdg = FigureCanvasQTAgg(fig) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) self.__plotWdg.setSizePolicy(sizePolicy) self.__frameLayout.addWidget(self.__plotWdg) def setProfiles(self, profiles, numLines): """ To set the profiles :param profiles: profiles : positions with elevations (for line and points) :param numLines: number of selected connected lines """ self.__numLines = numLines self.__profiles = profiles if self.__lib == 'Matplotlib': self.__prepare_points() def __getLinearPoints(self): """ To extract the linear points of the profile """ profileLen = 0 self.__profiles[0]['l'] = profileLen for i in range(0, len(self.__profiles)-1): x1 = float(self.__profiles[i]['x']) y1 = float(self.__profiles[i]['y']) x2 = float(self.__profiles[i+1]['x']) y2 = float(self.__profiles[i+1]['y']) profileLen += sqrt(((x2-x1)*(x2-x1)) + ((y2-y1)*(y2-y1))) self.__profiles[i+1]['l'] = profileLen def __getMnt(self, settings): """ To get the MN data for the profile :param settings: settings containing MN url """ if settings is None or settings.mntUrl is None or settings.mntUrl == "None": url = 'http://map.lausanne.ch/main/wsgi/profile.json' elif settings.mntUrl == "": return else: url = settings.mntUrl names = ['mnt', 'mns', 'toit_rocher'] url += '?layers=' pos = 0 for name in names: if pos > 0: url += ',' pos += 1 url += name url += '&geom={"type":"LineString","coordinates":[' pos = 0 for i in range(len(self.__profiles)): if pos > 0: url += ',' pos += 1 url += '[' + str(self.__profiles[i]['x']) + ',' + str(self.__profiles[i]['y']) + ']' url = url + ']}&nbPoints=' + str(int(self.__profiles[len(self.__profiles)-1]['l'])) try: response = urlopen(url) j = response.read() j_obj = json.loads(j) profile = j_obj['profile'] self.__mntPoints = [] self.__mntPoints.append(names) mnt_l = [] mnt_z = [] for p in range(len(names)): z = [] mnt_z.append(z) for pt in profile: mnt_l.append(float(pt['dist'])) values = pt['values'] for p in range(len(names)): if names[p] in values: mnt_z[p].append(float(values[names[p]])) else: mnt_z[p].append(None) self.__mntPoints.append(mnt_l) self.__mntPoints.append(mnt_z) except HTTPError as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "HTTP Error"), QCoreApplication.translate("VDLTools", "status error [" + str(e.code) + "] : " + e.reason), level=QgsMessageBar.CRITICAL, duration=0) except URLError as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "URL Error"), e.reason, level=QgsMessageBar.CRITICAL, duration=0) def attachCurves(self, names, settings, usedMnts): """ To attach the curves for the layers to the profile :param names: layers names """ if (self.__profiles is None) or (self.__profiles == 0): return self.__getLinearPoints() if usedMnts is not None and (usedMnts[0] or usedMnts[1] or usedMnts[2]): self.__getMnt(settings) c = 0 if self.__mntPoints is not None: for p in range(len(self.__mntPoints[0])): if usedMnts[p]: legend = QLabel("<font color='" + self.__colors[c] + "'>" + self.__mntPoints[0][p] + "</font>") self.__legendLayout.addWidget(legend) if self.__lib == 'Qwt5': xx = [list(g) for k, g in itertools.groupby(self.__mntPoints[1], lambda x: x is None) if not k] yy = [list(g) for k, g in itertools.groupby(self.__mntPoints[2][p], lambda x: x is None) if not k] for j in range(len(xx)): curve = QwtPlotCurve(self.__mntPoints[0][p]) curve.setData(xx[j], yy[j]) curve.setPen(QPen(QColor(self.__colors[c]), 3)) curve.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': qcol = QColor(self.__colors[c]) self.__plotWdg.figure.get_axes()[0].plot(self.__mntPoints[1], self.__mntPoints[2][p], gid=self.__mntPoints[0][p], linewidth=3) tmp = self.__plotWdg.figure.get_axes()[0].get_lines() for t in range(len(tmp)): if self.__mntPoints[0][p] == tmp[t].get_gid(): tmp[c].set_color((old_div(qcol.red(), 255.0), old_div(qcol.green(), 255.0), old_div(qcol.blue(), 255.0), old_div(qcol.alpha(), 255.0))) self.__plotWdg.draw() break c += 1 if 'z' in self.__profiles[0]: for i in range(len(self.__profiles[0]['z'])): if i < self.__numLines: v = 0 else: v = i - self.__numLines + 1 name = names[v] xx = [] yy = [] for prof in self.__profiles: xx.append(prof['l']) yy.append(prof['z'][i]) for j in range(len(yy)): if yy[j] is None: xx[j] = None if i == 0 or i > (self.__numLines-1): legend = QLabel("<font color='" + self.__colors[c] + "'>" + name + "</font>") self.__legendLayout.addWidget(legend) if self.__lib == 'Qwt5': # Split xx and yy into single lines at None values xx = [list(g) for k, g in itertools.groupby(xx, lambda x: x is None) if not k] yy = [list(g) for k, g in itertools.groupby(yy, lambda x: x is None) if not k] # Create & attach one QwtPlotCurve per one single line for j in range(len(xx)): curve = QwtPlotCurve(name) curve.setData(xx[j], yy[j]) curve.setPen(QPen(QColor(self.__colors[c]), 3)) if i > (self.__numLines-1): curve.setStyle(QwtPlotCurve.Dots) pen = QPen(QColor(self.__colors[c]), 8) pen.setCapStyle(Qt.RoundCap) curve.setPen(pen) curve.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': qcol = QColor(self.__colors[c]) if i < self.__numLines: self.__plotWdg.figure.get_axes()[0].plot(xx, yy, gid=name, linewidth=3) else: self.__plotWdg.figure.get_axes()[0].plot(xx, yy, gid=name, linewidth=5, marker='o', linestyle='None') tmp = self.__plotWdg.figure.get_axes()[0].get_lines() for t in range(len(tmp)): if name == tmp[t].get_gid(): tmp[c].set_color((old_div(qcol.red(), 255.0), old_div(qcol.green(), 255.0), old_div(qcol.blue(), 255.0), old_div(qcol.alpha(), 255.0))) self.__plotWdg.draw() break c += 1 # scaling this try: self.__reScalePlot(None, True) except: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Rescale problem... (trace printed)"), level=QgsMessageBar.CRITICAL, duration=0) print( QCoreApplication.translate("VDLTools", "rescale problem : "), sys.exc_info()[0], traceback.format_exc()) if self.__lib == 'Qwt5': self.__plotWdg.replot() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].redraw_in_frame() self.__plotWdg.draw() self.__activateMouseTracking(True) self.__marker.show() def __reScalePlot(self, value=None, auto=False): """ To rescale the profile plot depending to the bounds """ if (self.__profiles is None) or (self.__profiles == 0): self.__plotWdg.replot() return maxi = 0 for i in range(len(self.__profiles)): if (int(self.__profiles[i]['l'])) > maxi: maxi = int(self.__profiles[i]['l']) + 1 if self.__lib == 'Qwt5': self.__plotWdg.setAxisScale(2, 0, maxi, 0) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].set_xbound(0, maxi) minimumValue = self.__minSpin.value() maximumValue = self.__maxSpin.value() # to set max y and min y displayed if auto: minimumValue = 1000000000 maximumValue = -1000000000 for i in range(len(self.__profiles)): if 'z' in self.__profiles[i]: mini = self.__minTab(self.__profiles[i]['z']) if int(mini) < minimumValue: minimumValue = int(mini) - 1 maxi = self.__maxTab(self.__profiles[i]['z']) if int(maxi) > maximumValue: maximumValue = int(maxi) + 1 if self.__mntPoints is not None: for pts in self.__mntPoints[2]: miniMnt = self.__minTab(pts) if int(miniMnt) < minimumValue: minimumValue = int(miniMnt) - 1 maxiMnt = self.__maxTab(pts) if int(maxiMnt) > maximumValue: maximumValue = int(maxiMnt) + 1 self.__maxSpin.setValue(maximumValue) self.__minSpin.setValue(minimumValue) self.__maxSpin.setEnabled(True) self.__minSpin.setEnabled(True) if self.__lib == 'Qwt5': rect = QRectF(0, minimumValue, maxi, maximumValue-minimumValue) self.__zoomer.setZoomBase(rect) # to draw vertical lines for i in range(len(self.__profiles)): zz = [] for j in range(self.__numLines): if self.__profiles[i]['z'][j] is not None: zz.append(j) color = None if len(zz) == 2: width = 3 color = QColor('red') else: width = 1 if self.__lib == 'Qwt5': vertLine = QwtPlotMarker() vertLine.setLineStyle(QwtPlotMarker.VLine) pen = vertLine.linePen() pen.setWidth(width) if color is not None: pen.setColor(color) vertLine.setLinePen(pen) vertLine.setXValue(self.__profiles[i]['l']) label = vertLine.label() label.setText(str(i)) vertLine.setLabel(label) vertLine.setLabelAlignment(Qt.AlignLeft) vertLine.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].vlines(self.__profiles[i]['l'], minimumValue, maximumValue, linewidth=width) if minimumValue < maximumValue: if self.__lib == 'Qwt5': self.__plotWdg.setAxisScale(0, minimumValue, maximumValue, 0) self.__plotWdg.replot() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].set_ybound(minimumValue, maximumValue) self.__plotWdg.figure.get_axes()[0].redraw_in_frame() self.__plotWdg.draw() @staticmethod def __minTab(tab): """ To get the minimum value in a table :param tab: table to scan :return: minimum value """ mini = 1000000000 for t in tab: if t is None: continue if t < mini: mini = t return mini @staticmethod def __maxTab(tab): """ To get the maximum value in a table :param tab: table to scan :return: maximum value """ maxi = -1000000000 for t in tab: if t is None: continue if t > maxi: maxi = t return maxi def __setLib(self): """ To set the new widget library (qwt <-> matplotlib) """ self.__lib = self.__libs[self.__libCombo.currentIndex()] self.__changePlotWidget() def __save(self): """ To save the profile in a file, on selected format """ idx = self.__typeCombo.currentIndex() if idx == 0: self.__outPDF() elif idx == 1: self.__outPNG() else: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Invalid index ") + str(idx), level=QgsMessageBar.CRITICAL, duration=0) def __outPDF(self): """ To save the profile as pdf file """ fileName = QFileDialog.getSaveFileName( self.__iface.mainWindow(), QCoreApplication.translate("VDLTools", "Save As"), QCoreApplication.translate("VDLTools", "Profile.pdf"),"Portable Document Format (*.pdf)") if fileName is not None: if self.__lib == 'Qwt5': printer = QPrinter() printer.setCreator(QCoreApplication.translate("VDLTools", "QGIS Profile Plugin")) printer.setOutputFileName(fileName) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOrientation(QPrinter.Landscape) self.__plotWdg.print_(printer) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.savefig(str(fileName)) # printer = QPrinter() # printer.setCreator(QCoreApplication.translate("VDLTools", "QGIS Profile Plugin")) # printer.setOutputFileName(fileName) # printer.setOutputFormat(QPrinter.PdfFormat) # printer.setOrientation(QPrinter.Landscape) # printer.setPaperSize(QSizeF(self.__printWdg.size()), QPrinter.Millimeter) # printer.setFullPage(True) # self.__printWdg.render(printer) def __outPNG(self): """ To save the profile as png file """ fileName = QFileDialog.getSaveFileName( self.__iface.mainWindow(), QCoreApplication.translate("VDLTools", "Save As"), QCoreApplication.translate("VDLTools", "Profile.png"),"Portable Network Graphics (*.png)") if fileName is not None: QPixmap.grabWidget(self.__printWdg).save(fileName, "PNG") def clearData(self): """ To clear the displayed data """ if self.__profiles is None: return if self.__lib == 'Qwt5': self.__plotWdg.clear() self.__profiles = None temp1 = self.__plotWdg.itemList() for j in range(len(temp1)): if temp1[j].rtti() == QwtPlotItem.Rtti_PlotCurve: temp1[j].detach() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].cla() self.__manageMatplotlibAxe(self.__plotWdg.figure.get_axes()[0]) self.__maxSpin.setEnabled(False) self.__minSpin.setEnabled(False) self.__maxSpin.setValue(0) self.__minSpin.setValue(0) # clear legend while self.__legendLayout.count(): child = self.__legendLayout.takeAt(0) child.widget().deleteLater() def __manageMatplotlibAxe(self, axe): """ To manage the axes for matplotlib library :param axe: the axes element """ axe.grid() axe.tick_params(axis="both", which="major", direction="out", length=10, width=1, bottom=True, top=False, left=True, right=False) axe.minorticks_on() axe.tick_params(axis="both", which="minor", direction="out", length=5, width=1, bottom=True, top=False, left=True, right=False) axe.set_xlabel(QCoreApplication.translate("VDLTools", "Distance [m]")) axe.set_ylabel(QCoreApplication.translate("VDLTools", "Elevation [m]")) def __activateMouseTracking(self, activate): """ To (de)activate the mouse tracking on the profile for matplotlib library :param activate: true to activate, false to deactivate """ if activate: self.__doTracking = True self.__loadRubber() self.cid = self.__plotWdg.mpl_connect('motion_notify_event', self.__mouseevent_mpl) elif self.__doTracking: self.__doTracking = False self.__plotWdg.mpl_disconnect(self.cid) if self.__marker is not None: self.__canvas.scene().removeItem(self.__marker) try: if self.__vline is not None: self.__plotWdg.figure.get_axes()[0].lines.remove(self.__vline) self.__plotWdg.draw() except Exception as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Tracking exception : ") + str(e), level=QgsMessageBar.CRITICAL, duration=0) def __mouseevent_mpl(self, event): """ To manage matplotlib mouse tracking event :param event: mouse tracking event """ if event.xdata is not None: try: if self.__vline is not None: self.__plotWdg.figure.get_axes()[0].lines.remove(self.__vline) except Exception as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Mouse event exception : ") + str(e), level=QgsMessageBar.CRITICAL, duration=0) xdata = float(event.xdata) self.__vline = self.__plotWdg.figure.get_axes()[0].axvline(xdata, linewidth=2, color='k') self.__plotWdg.draw() i = 1 while i < len(self.__tabmouseevent)-1 and xdata > self.__tabmouseevent[i][0]: i += 1 i -= 1 x = self.__tabmouseevent[i][1] + (self.__tabmouseevent[i + 1][1] - self.__tabmouseevent[i][1]) / ( self.__tabmouseevent[i + 1][0] - self.__tabmouseevent[i][0]) * (xdata - self.__tabmouseevent[i][0]) y = self.__tabmouseevent[i][2] + (self.__tabmouseevent[i + 1][2] - self.__tabmouseevent[i][2]) / ( self.__tabmouseevent[i + 1][0] - self.__tabmouseevent[i][0]) * (xdata - self.__tabmouseevent[i][0]) self.__marker.show() self.__marker.setCenter(QgsPoint(x, y)) def __loadRubber(self): """ To load te rubber band for mouse tracking on map """ self.__marker = QgsVertexMarker(self.__canvas) self.__marker.setIconSize(5) self.__marker.setIconType(QgsVertexMarker.ICON_BOX) self.__marker.setPenWidth(3) def __prepare_points(self): """ To prepare the points on map for mouse tracking on profile """ self.__tabmouseevent = [] length = 0 for i, point in enumerate(self.__profiles): if i == 0: self.__tabmouseevent.append([0, point['x'], point['y']]) else: length += ((self.__profiles[i]['x'] - self.__profiles[i-1]['x']) ** 2 + (self.__profiles[i]['y'] - self.__profiles[i-1]['y']) ** 2) ** 0.5 self.__tabmouseevent.append([float(length), float(point['x']), float(point['y'])]) def closeEvent(self, event): """ When the dock widget is closed :param event: close event """ if self.__maxSpin is not None: Signal.safelyDisconnect(self.__maxSpin.valueChanged, self.__reScalePlot) self.__maxSpin = None if self.__minSpin is not None: Signal.safelyDisconnect(self.__minSpin.valueChanged, self.__reScalePlot) self.__minSpin = None if self.__saveButton is not None: Signal.safelyDisconnect(self.__saveButton.clicked, self.__save) self.__saveButton = None if self.__libCombo is not None: Signal.safelyDisconnect(self.__libCombo.currentIndexChanged, self.__setLib) self.__libCombo = None self.closeSignal.emit() if self.__marker is not None: self.__marker.hide() QDockWidget.closeEvent(self, event)
class calibrlogger(PyQt4.QtGui.QMainWindow, Calibr_Ui_Dialog): # An instance of the class Calibr_Ui_Dialog is created same time as instance of calibrlogger is created def __init__(self, parent, settingsdict1={}, obsid=''): PyQt4.QtGui.QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))#show the user this may take a long time... self.obsid = obsid self.log_pos = None self.y_pos = None self.meas_ts = None self.head_ts = None self.level_masl_ts = None self.loggerpos_masl_or_offset_state = 1 self.settingsdict = settingsdict1 PyQt4.QtGui.QDialog.__init__(self, parent) self.setAttribute(PyQt4.QtCore.Qt.WA_DeleteOnClose) self.setupUi(self) # Required by Qt4 to initialize the UI self.setWindowTitle("Calibrate logger") # Set the title for the dialog self.connect(self.pushButton, PyQt4.QtCore.SIGNAL("clicked()"), self.calibrateandplot) self.INFO.setText("Select the observation point with logger data to be calibrated.") self.log_calc_manual.setText("<a href=\"https://sites.google.com/site/midvattenpluginforqgis/usage/3-edit-data?pli=1#TOC-Calibrate-water-level-measurements-from-data-logger-\">Midvatten manual</a>") # Create a plot window with one single subplot self.calibrplotfigure = plt.figure() self.axes = self.calibrplotfigure.add_subplot( 111 ) self.canvas = FigureCanvas( self.calibrplotfigure ) self.mpltoolbar = NavigationToolbar( self.canvas, self.widgetPlot ) lstActions = self.mpltoolbar.actions() self.mpltoolbar.removeAction( lstActions[ 7 ] ) self.layoutplot.addWidget( self.canvas ) self.layoutplot.addWidget( self.mpltoolbar ) self.show() self.cid =[] self.connect(self.pushButtonFrom, PyQt4.QtCore.SIGNAL("clicked()"), self.set_from_date_from_x) self.connect(self.pushButtonTo, PyQt4.QtCore.SIGNAL("clicked()"), self.set_to_date_from_x) self.connect(self.pushButtonupdateplot, PyQt4.QtCore.SIGNAL("clicked()"), self.update_plot) self.connect(self.loggerpos_masl_or_offset, PyQt4.QtCore.SIGNAL("clicked()"), self.loggerpos_masl_or_offset_change) self.connect(self.pushButtonLpos, PyQt4.QtCore.SIGNAL("clicked()"), self.calibrate_from_plot_selection) self.connect(self.pushButtonCalcBestFit, PyQt4.QtCore.SIGNAL("clicked()"), self.calc_best_fit) self.connect(self.pushButton_delete_logger, PyQt4.QtCore.SIGNAL("clicked()"), lambda: self.delete_selected_range(u'w_levels_logger')) self.connect(self.pushButton_delete_meas, PyQt4.QtCore.SIGNAL("clicked()"), lambda: self.delete_selected_range(u'w_levels')) self.get_tolerance() # Populate combobox with obsid from table w_levels_logger self.load_obsid_from_db() PyQt4.QtGui.QApplication.restoreOverrideCursor()#now this long process is done and the cursor is back as normal def load_obsid_from_db(self): self.combobox_obsid.clear() myconnection = utils.dbconnection() if myconnection.connect2db() == True: # skapa en cursor curs = myconnection.conn.cursor() rs=curs.execute("""select distinct obsid from w_levels_logger order by obsid""") self.combobox_obsid.addItem('') for row in curs: self.combobox_obsid.addItem(row[0]) rs.close() myconnection.closedb() def load_obsid_and_init(self): """ Checks the current obsid and reloads all ts. :return: obsid Info: Before, some time series was only reloaded when the obsid was changed, but this caused a problem if the data was changed in the background in for example spatialite gui. Now all time series are reloaded always. It's rather fast anyway. """ obsid = unicode(self.combobox_obsid.currentText()) if not obsid: utils.pop_up_info("ERROR: no obsid is chosen") meas_sql = r"""SELECT date_time, level_masl FROM w_levels WHERE obsid = '""" + obsid + """' ORDER BY date_time""" self.meas_ts = self.sql_into_recarray(meas_sql) head_sql = r"""SELECT date_time as 'date [datetime]', head_cm / 100 FROM w_levels_logger WHERE obsid = '""" + obsid + """' ORDER BY date_time""" self.head_ts = self.sql_into_recarray(head_sql) self.obsid = obsid level_masl_ts_sql = r"""SELECT date_time as 'date [datetime]', level_masl FROM w_levels_logger WHERE obsid = '""" + self.obsid + """' ORDER BY date_time""" self.level_masl_ts = self.sql_into_recarray(level_masl_ts_sql) return obsid def getlastcalibration(self): obsid = self.load_obsid_and_init() if not obsid=='': sql = """SELECT MAX(date_time), loggerpos FROM (SELECT date_time, (level_masl - (head_cm/100)) as loggerpos FROM w_levels_logger WHERE level_masl > -990 AND obsid = '""" sql += obsid sql += """')""" self.lastcalibr = utils.sql_load_fr_db(sql)[1] if self.lastcalibr[0][1] and self.lastcalibr[0][0]: text = """Last pos. for logger in """ text += obsid text += """\nwas """ + str(self.lastcalibr[0][1]) + """ masl\nat """ + str(self.lastcalibr[0][0]) else: text = """There is no earlier known\nposition for the logger\nin """ + unicode(self.combobox_obsid.currentText())#self.obsid[0] self.INFO.setText(text) def calibrateandplot(self): obsid = self.load_obsid_and_init() if not self.LoggerPos.text() == '': self.calibrate() self.update_plot() # def calibrate(self, fr_d_t=self.FromDateTime.dateTime().toPyDateTime(), to_d_t=self.ToDateTime.dateTime().toPyDateTime()): def calibrate(self): self.calib_help.setText("Calibrating") PyQt4.QtGui.QApplication.setOverrideCursor(PyQt4.QtCore.Qt.WaitCursor) obsid = self.load_obsid_and_init() if not obsid=='': sanity1sql = """select count(obsid) from w_levels_logger where obsid = '""" + obsid[0] + """'""" sanity2sql = """select count(obsid) from w_levels_logger where head_cm not null and head_cm !='' and obsid = '""" + obsid[0] + """'""" if utils.sql_load_fr_db(sanity1sql)[1] == utils.sql_load_fr_db(sanity2sql)[1]: # This must only be done if head_cm exists for all data fr_d_t = self.FromDateTime.dateTime().toPyDateTime() to_d_t = self.ToDateTime.dateTime().toPyDateTime() if self.loggerpos_masl_or_offset_state == 1: self.update_level_masl_from_head(obsid, fr_d_t, to_d_t, self.LoggerPos.text()) else: self.update_level_masl_from_level_masl(obsid, fr_d_t, to_d_t, self.LoggerPos.text()) self.getlastcalibration() else: utils.pop_up_info("Calibration aborted!!\nThere must not be empty cells or\nnull values in the 'head_cm' column!") else: self.INFO.setText("Select the observation point with logger data to be calibrated.") self.calib_help.setText("") PyQt4.QtGui.QApplication.restoreOverrideCursor() def update_level_masl_from_level_masl(self, obsid, fr_d_t, to_d_t, newzref): """ Updates the level masl using newzref :param obsid: (str) The obsid :param fr_d_t: (datetime) start of calibration :param to_d_t: (datetime) end of calibration :param newzref: (int/float/str [m]) The correction that should be made against the head [m] :return: None """ sql =r"""UPDATE w_levels_logger SET level_masl = """ sql += str(newzref) sql += """ + level_masl WHERE obsid = '""" sql += obsid # Sqlite seems to have problems with date comparison date_time >= a_date, so they have to be converted into total seconds first. sql += """' AND CAST(strftime('%s', date_time) AS NUMERIC) >= """ sql += str((fr_d_t - datetime.datetime(1970,1,1)).total_seconds()) sql += """ AND CAST(strftime('%s', date_time) AS NUMERIC) <= """ sql += str((to_d_t - datetime.datetime(1970,1,1)).total_seconds()) sql += """ """ dummy = utils.sql_alter_db(sql) def update_level_masl_from_head(self, obsid, fr_d_t, to_d_t, newzref): """ Updates the level masl using newzref :param obsid: (str) The obsid :param fr_d_t: (datetime) start of calibration :param to_d_t: (datetime) end of calibration :param newzref: (int/float/str [m]) The correction that should be made against the head [m] :return: None """ sql =r"""UPDATE w_levels_logger SET level_masl = """ sql += str(newzref) sql += """ + head_cm / 100 WHERE obsid = '""" sql += obsid # Sqlite seems to have problems with date comparison date_time >= a_date, so they have to be converted into total seconds first. sql += """' AND CAST(strftime('%s', date_time) AS NUMERIC) >= """ sql += str((fr_d_t - datetime.datetime(1970,1,1)).total_seconds()) sql += """ AND CAST(strftime('%s', date_time) AS NUMERIC) <= """ sql += str((to_d_t - datetime.datetime(1970,1,1)).total_seconds()) sql += """ """ dummy = utils.sql_alter_db(sql) def sql_into_recarray(self, sql): """ Converts and runs an sql-string and turns the answer into an np.recarray and returns it""" my_format = [('date_time', datetime.datetime), ('values', float)] #Define (with help from function datetime) a good format for numpy array recs = utils.sql_load_fr_db(sql)[1] table = np.array(recs, dtype=my_format) #NDARRAY table2=table.view(np.recarray) # RECARRAY Makes the two columns inte callable objects, i.e. write table2.values return table2 def update_plot(self): """ Plots self.level_masl_ts, self.meas_ts and maybe self.head_ts """ self.reset_plot_selects_and_calib_help() self.calib_help.setText("Updating plot") PyQt4.QtGui.QApplication.setOverrideCursor(PyQt4.QtCore.Qt.WaitCursor) obsid = self.load_obsid_and_init() self.axes.clear() p=[None]*2 # List for plot objects # Load manual reading (full time series) for the obsid self.plot_recarray(self.axes, self.meas_ts, obsid, 'o-', 10) # Load Loggerlevels (full time series) for the obsid if self.loggerLineNodes.isChecked(): logger_line_style = '.-' else: logger_line_style = '-' self.plot_recarray(self.axes, self.level_masl_ts, obsid + unicode(' logger', 'utf-8'), logger_line_style, 10) #Plot the original head_cm if self.plot_logger_head.isChecked(): self.plot_recarray(self.axes, self.head_ts, obsid + unicode(' original logger head', 'utf-8'), logger_line_style, 10) """ Finish plot """ self.axes.grid(True) self.axes.yaxis.set_major_formatter(tick.ScalarFormatter(useOffset=False, useMathText=False)) self.calibrplotfigure.autofmt_xdate() self.axes.set_ylabel(unicode('Level (masl)', 'utf-8')) #This is the method that accepts even national characters ('åäö') in matplotlib axes labels self.axes.set_title(unicode('Calibration plot for ', 'utf-8') + str(obsid)) #This is the method that accepts even national characters ('åäö') in matplotlib axes labels for label in self.axes.xaxis.get_ticklabels(): label.set_fontsize(10) for label in self.axes.yaxis.get_ticklabels(): label.set_fontsize(10) #plt.show() self.canvas.draw() plt.close(self.calibrplotfigure)#this closes reference to self.calibrplotfigure PyQt4.QtGui.QApplication.restoreOverrideCursor() self.calib_help.setText("") def plot_recarray(self, axes, a_recarray, lable, line_style, picker=10): """ Plots a recarray to the supplied axes object """ # Get help from function datestr2num to get date and time into float myTimestring = [a_recarray.date_time[idx] for idx in xrange(len(a_recarray))] numtime=datestr2num(myTimestring) #conv list of strings to numpy.ndarray of floats axes.plot_date(numtime, a_recarray.values, line_style, label=lable, picker=picker) def set_from_date_from_x(self): """ Used to set the self.FromDateTime by clicking on a line node in the plot self.canvas """ self.reset_plot_selects_and_calib_help() self.calib_help.setText("Select a node to use as \"from\"") self.deactivate_pan_zoom() self.canvas.setFocusPolicy(Qt.ClickFocus) self.canvas.setFocus() self.cid.append(self.canvas.mpl_connect('pick_event', lambda event: self.set_date_from_x_onclick(event, self.FromDateTime))) def set_to_date_from_x(self): """ Used to set the self.ToDateTime by clicking on a line node in the plot self.canvas """ self.reset_plot_selects_and_calib_help() self.calib_help.setText("Select a node to use as \"to\"") self.deactivate_pan_zoom() self.canvas.setFocusPolicy(Qt.ClickFocus) self.canvas.setFocus() self.cid.append(self.canvas.mpl_connect('pick_event', lambda event: self.set_date_from_x_onclick(event, self.ToDateTime))) def set_date_from_x_onclick(self, event, date_holder): """ Sets the date_holder to a date from a line node closest to the pick event date_holder: a QDateTimeEdit object. """ found_date = utils.find_nearest_date_from_event(event) date_holder.setDateTime(found_date) self.reset_plot_selects_and_calib_help() def reset_plot_selects_and_calib_help(self): """ Reset self.cid and self.calib_help """ self.reset_cid() self.log_pos = None self.y_pos = None self.calib_help.setText("") def reset_cid(self): """ Resets self.cid to an empty list and disconnects unused events """ for x in self.cid: self.canvas.mpl_disconnect(x) self.cid = [] def calibrate_from_plot_selection(self): """ Calibrates by selecting a line node and a y-position on the plot The user have to click on the button three times and follow instructions. The process: 1. Selecting a line node. 2. Selecting a selecting a y-position from the plot. 3. Extracting the head from head_ts with the same date as the line node. 4. Calculating y-position - head (or level_masl) and setting self.LoggerPos. 5. Run calibration. """ #Run init to make sure self.meas_ts and self.head_ts is updated for the current obsid. self.load_obsid_and_init() self.deactivate_pan_zoom() self.canvas.setFocusPolicy(Qt.ClickFocus) self.canvas.setFocus() if self.log_pos is None: self.calib_help.setText("Select a logger node.") self.cid.append(self.canvas.mpl_connect('pick_event', self.set_log_pos_from_node_date_click)) if self.log_pos is not None and self.y_pos is None: self.calib_help.setText("Select a y position to move to.") self.cid.append(self.canvas.mpl_connect('button_press_event', self.set_y_pos_from_y_click)) if self.log_pos is not None and self.y_pos is not None: PyQt4.QtGui.QApplication.setOverrideCursor(PyQt4.QtCore.Qt.WaitCursor) if self.loggerpos_masl_or_offset_state == 1: logger_ts = self.head_ts else: logger_ts = self.level_masl_ts y_pos = self.y_pos log_pos = self.log_pos self.y_pos = None self.log_pos = None log_pos_date = datestring_to_date(log_pos).replace(tzinfo=None) logger_value = None #Get the value for the selected node for idx, date_value_tuple in enumerate(logger_ts): raw_date, logger_value = date_value_tuple date = datestring_to_date(raw_date).replace(tzinfo=None) if date == log_pos_date: break if logger_value is None: utils.pop_up_info("No connection between head_ts dates and logger date could be made!\nTry again or choose a new logger line node!") else: self.LoggerPos.setText(str(float(y_pos) - float(logger_value))) PyQt4.QtGui.QApplication.restoreOverrideCursor() self.calibrateandplot() self.calib_help.setText("") def set_log_pos_from_node_date_click(self, event): """ Sets self.log_pos variable to the date (x-axis) from the node nearest the pick event """ found_date = utils.find_nearest_date_from_event(event) self.calib_help.setText("Logger node " + str(found_date) + " selected, click button \"Calibrate by selection in plot\" again.") self.log_pos = found_date self.reset_cid() def set_y_pos_from_y_click(self, event): """ Sets the self.y_pos variable to the y value of the click event """ self.y_pos = event.ydata self.calib_help.setText("Y position set, click button \"Calibrate by selection in plot\" again for calibration.") self.reset_cid() def calc_best_fit(self): """ Calculates the self.LoggerPos from self.meas_ts and self.head_ts First matches measurements from self.meas_ts to logger values from self.head_ts. This is done by making a mean of all logger values inside self.meas_ts date - tolerance and self.meas_ts date + tolerance. (this could probably be change to get only the closest logger value inside the tolerance instead) (Tolerance is gotten from self.get_tolerance()) Then calculates the mean of all matches and set to self.LoggerPos. """ obsid = self.load_obsid_and_init() self.reset_plot_selects_and_calib_help() tolerance = self.get_tolerance() really_calibrate_question = utils.askuser("YesNo", """This will calibrate all values inside the chosen period\nusing the mean difference between logger values and measurements.\n\nTime tolerance for matching logger and measurement nodes set to '""" + ' '.join(tolerance) + """'\n\nContinue?""") if really_calibrate_question.result == 0: # if the user wants to abort return PyQt4.QtGui.QApplication.setOverrideCursor(PyQt4.QtCore.Qt.WaitCursor) if self.loggerpos_masl_or_offset_state == 1: logger_ts = self.head_ts else: logger_ts = self.level_masl_ts coupled_vals = self.match_ts_values(self.meas_ts, logger_ts, tolerance) if not coupled_vals: utils.pop_up_info("There was no matched measurements or logger values inside the chosen period.\n Try to increase the tolerance!") else: self.LoggerPos.setText(str(utils.calc_mean_diff(coupled_vals))) self.calibrateandplot() PyQt4.QtGui.QApplication.restoreOverrideCursor() def match_ts_values(self, meas_ts, logger_ts, tolerance): """ Matches two timeseries values for shared timesteps For every measurement point, a mean of logger values inside measurementpoint + x minutes to measurementpoint - x minutes is coupled together. At the first used measurement, only logger values greater than the set start date is used. At the last measurement, only logger values lesser than the set end date is used. This is done so that values from another logger reposition is not mixed with the chosen logger positioning. (Hard to explain). """ coupled_vals = [] #Get the tolerance, default to 10 minutes tol = int(tolerance[0]) tol_period = tolerance[1] logger_gen = utils.ts_gen(logger_ts) try: l = next(logger_gen) except StopIteration: return None log_vals = [] all_done = False #The .replace(tzinfo=None) is used to remove info about timezone. Needed for the comparisons. This should not be a problem though as the date scale in the plot is based on the dates from the database. outer_begin = self.FromDateTime.dateTime().toPyDateTime().replace(tzinfo=None) outer_end = self.ToDateTime.dateTime().toPyDateTime().replace(tzinfo=None) logger_step = datestring_to_date(l[0]).replace(tzinfo=None) for m in meas_ts: if logger_step is None: break meas_step = datestring_to_date(m[0]).replace(tzinfo=None) step_begin = dateshift(meas_step, -tol, tol_period) step_end = dateshift(meas_step, tol, tol_period) if step_end < outer_begin: continue if step_begin > outer_end: break #Skip logger steps that are earlier than the chosen begin date or are not inside the measurement period. while logger_step < step_begin or logger_step < outer_begin: try: l = next(logger_gen) except StopIteration: all_done = True break logger_step = datestring_to_date(l[0]).replace(tzinfo=None) log_vals = [] while logger_step is not None and logger_step <= step_end and logger_step <= outer_end: if not math.isnan(float(l[1])) or l[1] == 'nan' or l[1] == 'NULL': log_vals.append(float(l[1])) try: l = next(logger_gen) except StopIteration: all_done = True break logger_step = datestring_to_date(l[0]).replace(tzinfo=None) if log_vals: mean = np.mean(log_vals) if not math.isnan(mean): coupled_vals.append((m[1], mean)) if all_done: break return coupled_vals def get_tolerance(self): """ Get the period tolerance, default to 10 minutes """ if not self.bestFitTolerance.text(): tol = '10 minutes' self.bestFitTolerance.setText(tol) else: tol = self.bestFitTolerance.text() tol_splitted = tol.split() if len(tol_splitted) != 2: utils.pop_up_info("Must write time resolution also, ex. 10 minutes") return tuple(tol_splitted) def loggerpos_masl_or_offset_change(self): if self.loggerpos_masl_or_offset_state == 1: self.label_11.setText("Offset relative to calibrated values:") self.loggerpos_masl_or_offset.setText("Change to logger position") self.label_adjustment_info.setText("Adjustments made from calibrated values") self.loggerpos_masl_or_offset_state = 0 else: self.label_11.setText("Logger position, masl:") self.loggerpos_masl_or_offset.setText("Change to offset") self.label_adjustment_info.setText("Adjustments made from head") self.loggerpos_masl_or_offset_state = 1 def deactivate_pan_zoom(self): """ Deactivates the NavigationToolbar pan or zoom feature if they are currently active """ if self.mpltoolbar._active == "PAN": self.mpltoolbar.pan() elif self.mpltoolbar._active == "ZOOM": self.mpltoolbar.zoom() def delete_selected_range(self, table_name): """ Deletes the current selected range from the database from w_levels_logger :return: De """ current_loaded_obsid = self.obsid selected_obsid = self.load_obsid_and_init() if current_loaded_obsid != selected_obsid: utils.pop_up_info("Error!\n The obsid selection has been changed but the plot has not been updated. No deletion done.\nUpdating plot.") self.update_plot() return fr_d_t = str((self.FromDateTime.dateTime().toPyDateTime() - datetime.datetime(1970,1,1)).total_seconds()) to_d_t = str((self.ToDateTime.dateTime().toPyDateTime() - datetime.datetime(1970,1,1)).total_seconds()) sql_list = [] sql_list.append(r"""delete from "%s" """%table_name) sql_list.append(r"""where obsid = '%s' """%selected_obsid) sql_list.append(r"""AND CAST(strftime('%s', date_time) AS NUMERIC) """) sql_list.append(r""" >= '%s' """%fr_d_t) sql_list.append(r"""AND CAST(strftime('%s', date_time) AS NUMERIC) """) sql_list.append(r""" <= '%s' """%to_d_t) sql = ''.join(sql_list) really_delete = utils.askuser("YesNo", "Do you want to delete the period " + str(self.FromDateTime.dateTime().toPyDateTime()) + " to " + str(self.ToDateTime.dateTime().toPyDateTime()) + " for obsid " + selected_obsid + " from table " + table_name + "?").result if really_delete: utils.sql_alter_db(sql) self.update_plot()
def mouseEventCallback(self, callback): if self.cid != None: FigureCanvas.mpl_disconnect(self, self.cid) self.cid = FigureCanvas.mpl_connect(self, 'button_press_event', callback)
class ROISelect(QMdiSubWindow): ########### # Signals # ########### plotSignal = pyqtSignal() ######################## # Initializing Methods # ######################## def __init__(self, title, imgfile, control, parent=None): ''' Initializes internal variables ''' QMdiSubWindow.__init__(self, parent) self.setWindowTitle(title) self.imgfile = imgfile self.control = control self.xys = [] self.ROI = np.zeros((600, 800)).astype(np.bool) for i in range(800): for j in range(600): self.xys.append((i, j)) self.create_main_frame() self.onDraw() def create_main_frame(self): ''' Creates the main window ''' # Widgets self.main_frame = QWidget() self.fig = Figure() self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.main_frame) self.axes = self.fig.add_subplot(111) self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame) self.reset = QPushButton('&Reset') # Connections self.cid = self.canvas.mpl_connect('button_press_event', self.onpress) self.control.imageChanged.connect(self.onImageChanged) self.control.closeSignal.connect(self.close) self.connect(self.reset, SIGNAL('clicked()'), self.onReset) # Layouts vbox = QVBoxLayout() vbox.addWidget(self.canvas) vbox.addWidget(self.mpl_toolbar) vbox.addWidget(self.reset) self.main_frame.setLayout(vbox) self.setWidget(self.main_frame) ######### # Slots # ######### def onReset(self): self.ROI = np.zeros((600, 800)).astype(np.bool) self.cid = self.canvas.mpl_connect('button_press_event', self.onpress) self.onDraw() def onDraw(self): self.axes.clear() img = Image.open(self.imgfile) img = np.asarray(img) self.axes.imshow(img) self.axes.imshow(self.ROI, alpha=0.1, cmap='gray') self.plotSignal.emit() self.canvas.draw() def getROI(self, verts): ind = points_inside_poly(self.xys, verts) self.canvas.draw_idle() self.canvas.widgetlock.release(self.lasso) del self.lasso self.ROI = ind self.ROI = self.ROI.reshape((600, 800), order='F') self.canvas.mpl_disconnect(self.cid) self.onDraw() def onpress(self, event): if self.canvas.widgetlock.locked(): return if event.inaxes is None: return self.lasso = Lasso(event.inaxes, (event.xdata, event.ydata), self.getROI) self.canvas.widgetlock(self.lasso) def onImageChanged(self, filename): ''' Catches the signal from the ControlPanel that the current image has changed ''' self.imgfile = str(filename) self.onDraw()
class PlotCanvas: """ Class handling the plotting area in the application. """ def __init__(self, container): """ The constructor configures the Matplotlib figure that will contain all plots, creates the base axes and connects events to the plotting area. :param container: The parent container in which to draw plots. :rtype: PlotCanvas """ # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels # Parent container self.container = container # Plots go onto a single matplotlib.figure self.figure = Figure(dpi=50) # TODO: dpi needed? self.figure.patch.set_visible(False) # These axes show the ticks and grid. No plotting done here. # New axes must have a label, otherwise mpl returns an existing one. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0) self.axes.set_aspect(1) self.axes.grid(True) # The canvas is the top level container (Gtk.DrawingArea) self.canvas = FigureCanvas(self.figure) # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # self.canvas.setFocus() #self.canvas.set_hexpand(1) #self.canvas.set_vexpand(1) #self.canvas.set_can_focus(True) # For key press # Attach to parent #self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns?? self.container.addWidget(self.canvas) # Qt # Copy a bitmap of the canvas for quick animation. # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) self.canvas.mpl_connect('button_release_event', self.on_mouse_release) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) #self.canvas.connect('configure-event', self.auto_adjust_axes) self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) #self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK) #self.canvas.connect("scroll-event", self.on_scroll) self.canvas.mpl_connect('scroll_event', self.on_scroll) self.canvas.mpl_connect('key_press_event', self.on_key_down) self.canvas.mpl_connect('key_release_event', self.on_key_up) self.canvas.mpl_connect('draw_event', self.on_draw) self.mouse = [0, 0] self.key = None self.pan_axes = [] self.panning = False def on_key_down(self, event): """ :param event: :return: """ FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key)) self.key = event.key def on_key_up(self, event): """ :param event: :return: """ self.key = None def mpl_connect(self, event_name, callback): """ Attach an event handler to the canvas through the Matplotlib interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: func :return: Connection id :rtype: int """ return self.canvas.mpl_connect(event_name, callback) def mpl_disconnect(self, cid): """ Disconnect callback with the give id. :param cid: Callback id. :return: None """ self.canvas.mpl_disconnect(cid) def connect(self, event_name, callback): """ Attach an event handler to the canvas through the native GTK interface. :param event_name: Name of the event :type event_name: str :param callback: Function to call :type callback: function :return: Nothing """ self.canvas.connect(event_name, callback) def clear(self): """ Clears axes and figure. :return: None """ # Clear self.axes.cla() try: self.figure.clf() except KeyError: FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()") # Re-build self.figure.add_axes(self.axes) self.axes.set_aspect(1) self.axes.grid(True) # Re-draw self.canvas.draw_idle() def adjust_axes(self, xmin, ymin, xmax, ymax): """ Adjusts all axes while maintaining the use of the whole canvas and an aspect ratio to 1:1 between x and y axes. The parameters are an original request that will be modified to fit these restrictions. :param xmin: Requested minimum value for the X axis. :type xmin: float :param ymin: Requested minimum value for the Y axis. :type ymin: float :param xmax: Requested maximum value for the X axis. :type xmax: float :param ymax: Requested maximum value for the Y axis. :type ymax: float :return: None """ # FlatCAMApp.App.log.debug("PC.adjust_axes()") width = xmax - xmin height = ymax - ymin try: r = width / height except ZeroDivisionError: FlatCAMApp.App.log.error("Height is %f" % height) return canvas_w, canvas_h = self.canvas.get_width_height() canvas_r = float(canvas_w) / canvas_h x_ratio = float(self.x_margin) / canvas_w y_ratio = float(self.y_margin) / canvas_h if r > canvas_r: ycenter = (ymin + ymax) / 2.0 newheight = height * r / canvas_r ymin = ycenter - newheight / 2.0 ymax = ycenter + newheight / 2.0 else: xcenter = (xmax + xmin) / 2.0 newwidth = width * canvas_r / r xmin = xcenter - newwidth / 2.0 xmax = xcenter + newwidth / 2.0 # Adjust axes for ax in self.figure.get_axes(): if ax._label != 'base': ax.set_frame_on(False) # No frame ax.set_xticks([]) # No tick ax.set_yticks([]) # No ticks ax.patch.set_visible(False) # No background ax.set_aspect(1) ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) # Sync re-draw to proper paint on form resize self.canvas.draw() def auto_adjust_axes(self, *args): """ Calls ``adjust_axes()`` using the extents of the base axes. :rtype : None :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() self.adjust_axes(xmin, ymin, xmax, ymax) def zoom(self, factor, center=None): """ Zooms the plot by factor around a given center point. Takes care of re-drawing. :param factor: Number by which to scale the plot. :type factor: float :param center: Coordinates [x, y] of the point around which to scale the plot. :type center: list :return: None """ xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin if center is None or center == [None, None]: center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0] # For keeping the point at the pointer location relx = (xmax - center[0]) / width rely = (ymax - center[1]) / height new_width = width / factor new_height = height / factor xmin = center[0] - new_width * (1 - relx) xmax = center[0] + new_width * relx ymin = center[1] - new_height * (1 - rely) ymax = center[1] + new_height * rely # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) # Async re-draw self.canvas.draw_idle() def pan(self, x, y): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() width = xmax - xmin height = ymax - ymin # Adjust axes for ax in self.figure.get_axes(): ax.set_xlim((xmin + x * width, xmax + x * width)) ax.set_ylim((ymin + y * height, ymax + y * height)) # Re-draw self.canvas.draw_idle() def new_axes(self, name): """ Creates and returns an Axes object attached to this object's Figure. :param name: Unique label for the axes. :return: Axes attached to the figure. :rtype: Axes """ return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name) def on_scroll(self, event): """ Scroll event handler. :param event: Event object containing the event information. :return: None """ # So it can receive key presses # self.canvas.grab_focus() self.canvas.setFocus() # Event info # z, direction = event.get_scroll_direction() if self.key is None: if event.button == 'up': self.zoom(1.5, self.mouse) else: self.zoom(1 / 1.5, self.mouse) return if self.key == 'shift': if event.button == 'up': self.pan(0.3, 0) else: self.pan(-0.3, 0) return if self.key == 'control': if event.button == 'up': self.pan(0, 0.3) else: self.pan(0, -0.3) return def on_mouse_press(self, event): # Check for middle mouse button press if event.button == 2: # Prepare axes for pan (using 'matplotlib' pan function) self.pan_axes = [] for a in self.figure.get_axes(): if (event.x is not None and event.y is not None and a.in_axes(event) and a.get_navigate() and a.can_pan()): a.start_pan(event.x, event.y, 1) self.pan_axes.append(a) # Set pan view flag if len(self.pan_axes) > 0: self.panning = True; def on_mouse_release(self, event): # Check for middle mouse button release to complete pan procedure if event.button == 2: for a in self.pan_axes: a.end_pan() # Clear pan flag self.panning = False def on_mouse_move(self, event): """ Mouse movement event hadler. Stores the coordinates. Updates view on pan. :param event: Contains information about the event. :return: None """ self.mouse = [event.xdata, event.ydata] # Update pan view on mouse move if self.panning is True: for a in self.pan_axes: a.drag_pan(1, event.key, event.x, event.y) # Async re-draw (redraws only on thread idle state, uses timer on backend) self.canvas.draw_idle() def on_draw(self, renderer): # Store background on canvas redraw self.background = self.canvas.copy_from_bbox(self.axes.bbox)
class roiSelectionTool(QtGui.QWidget): def __init__(self, filelist, rois, parent=None): super(roiSelectionTool, self).__init__(parent) self.rois = rois # a figure instance to plot on self.figure = plt.figure() self.filelist = filelist self.currentImage = 0 # number in list of current images # this is the Canvas Widget that displays the `figure` # it takes the `figure` instance as a parameter to __init__ #generate list widget self.listW = QtGui.QListWidget() self.listW.addItems(self.rois) self.colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1), (1, 0.55, 0.55), (0.55, 0.55, 0.55), (0.5, 0, 0), (1, 0.5, 0)] for index in xrange(self.listW.count()): self.listW.item(index).setTextColor( QtGui.QColor(self.colors[index][0] * 255, self.colors[index][1] * 255, self.colors[index][2] * 255)) self.listW.setCurrentRow(0) #self.setMaximumWidth(10000) #generate canvas self.canvas = FigureCanvas(self.figure) self.cid1 = self.canvas.mpl_connect('button_press_event', self.onPressCanvas) #self.canvas.setMaximumSize(1000000, 10000000) #generate scrollbar self.scroll = QtGui.QScrollBar() self.scroll.valueChanged.connect(self.changeSlice) self.scroll.setMaximum(255) self.scroll.setOrientation(1) self.scroll.setMaximumSize(300000, 50) #self.scroll.sliderMoved.connect(1) # this is the Navigation widget # it takes the Canvas widget and a parent self.toolbar = NavigationToolbar(self.canvas, self) #radio buttons choosing image type self.imageTypeBox = QtGui.QGroupBox('Choose Image') imagetypes = ['StDev', 'Max', 'Sum', 'Stack'] radioboxForm = QtGui.QFormLayout() self.radioImageType = {} for ctype in imagetypes: cr = QtGui.QRadioButton(ctype) cr.clicked.connect(self.updateImage) self.radioImageType[ctype] = cr radioboxForm.addRow(cr) if ctype == 'StDev': cr.setChecked(1) self.imageTypeBox.setLayout(radioboxForm) #import pdb; pdb.set_trace() #self.imageTypeBox.clicked.connect(self.currentImageType) #self.imageTypeBox.mousePressEvent(self.currentImageType) #radio buttons choosing iROI tool self.roiTypeBox = QtGui.QGroupBox('Choose ROI') roiTypes = ['lasso', 'polygon', 'circle', 'square'] radioboxForm = QtGui.QFormLayout() self.radioRoiTypes = {} for ctype in roiTypes: cr = QtGui.QRadioButton(ctype) self.radioRoiTypes[ctype] = cr radioboxForm.addRow(cr) if ctype == 'lasso': cr.setChecked(1) self.roiTypeBox.setLayout(radioboxForm) # buttons to change image currently viewed self.nextImgB = QtGui.QPushButton('Next Image') self.nextImgB.clicked.connect(self.nextImage) self.nextImgB.setMaximumSize(200, 50) self.prevImgB = QtGui.QPushButton('Previous Image') self.prevImgB.clicked.connect(self.prevImage) self.prevImgB.setMaximumSize(200, 50) self.eraseMaskB = QtGui.QPushButton('Erase Mask') self.eraseMaskB.clicked.connect(self.eraseMask) self.eraseMaskB.setMaximumSize(200, 50) # buttons to control contrast self.contrastB = QtGui.QPushButton('Contrast') self.contrastB.clicked.connect(self.contrast) self.contrastB.setMaximumSize(200, 50) # set the layout self.grid = QtGui.QGridLayout() self.grid.setSpacing(5) #first is row and next is column self.grid.addWidget(self.toolbar, 0, 0) self.grid.addWidget(self.canvas, 1, 0, 4, 3) self.grid.addWidget(self.nextImgB, 0, 2) self.grid.addWidget(self.prevImgB, 0, 1) self.grid.addWidget(self.scroll, 5, 0, 5, 3) self.grid.addWidget(self.listW, 0, 3, 2, 3) self.grid.addWidget(self.imageTypeBox, 3, 3) self.grid.addWidget(self.roiTypeBox, 4, 3) self.grid.addWidget(self.eraseMaskB, 4, 4) self.grid.addWidget(self.contrastB, 5, 4) self.grid.setColumnStretch(1, 4) self.grid.setRowStretch(3, 1) #self.grid.setColumnStretch() self.setLayout(self.grid) self.showMaximized() self.LoadImage() def eraseMask(self): savefile = os.path.split(self.imgpath) savefile = os.path.join( savefile[0], str(self.listW.currentItem().text()) + '_Mask.npy') print('Deleting_' + savefile) os.remove(savefile) self.drawROI() def saveMask(self): savefile = os.path.split(self.imgpath) savefile = os.path.join( savefile[0], str(self.listW.currentItem().text()) + '_Mask.npy') print('Saving_' + savefile) np.save(savefile, self.IndexMask) def onPressCanvas(self, event): print("Event detected starting whatever tool is selected") self.canvas.mpl_disconnect(self.cid1) if self.canvas.widgetlock.locked(): return if event.inaxes is None: return if self.radioRoiTypes['lasso'].isChecked(): self.lasso = LassoDB(event.inaxes, (event.xdata, event.ydata), self.generateMask) elif self.radioRoiTypes['polygon'].isChecked(): #self.polygon = polygonCreator2(self.canvas, self.ax, (event.xdata, event.ydata), self.generateMask) self.polygon = polygonCreator(event.inaxes, (event.xdata, event.ydata), self.generateMask) else: print('Tool not implemented') # acquire a lock on the widget drawing #self.canvas.widgetlock(self.lasso) def generateMask(self, verts): p = matplotlib.lines.Path(verts) pix1 = np.arange(self.img.shape[1]) pix2 = np.arange(self.img.shape[2]) xv, yv = np.meshgrid(pix1, pix2) pix = np.vstack((xv.flatten(), yv.flatten())).T ind = p.contains_points(pix, radius=1) savefile = os.path.split(self.imgpath) savefile = os.path.join( savefile[0], str(self.listW.currentItem().text()) + '_Mask.npy') if os.path.isfile(savefile): index = np.load(savefile) self.Mask = np.zeros([self.img.shape[1], self.img.shape[2]]) if len(index) != 0: self.Mask[(index[0], index[1])] = 1 else: self.Mask = np.zeros([self.img.shape[1], self.img.shape[2]]) lin = np.arange(self.Mask.size) newArray = self.Mask.flatten() newArray[lin[ind]] = 1 #newArray[indices] = 1 self.Mask = newArray.reshape(self.Mask.shape) self.IndexMask = np.where(self.Mask == 1) self.saveMask() self.drawROI() self.cid1 = self.canvas.mpl_connect('button_press_event', self.onPressCanvas) #del self.lasso def drawROI(self): #remove old plot if 'plottedlines' in dir(self): for i, line1 in enumerate(self.plottedlines): #import pdb; pdb.set_trace() self.ax.lines.remove(line1[0]) #del self.ax.lines(i) #line1.remove() #plot masks self.plottedlines = [] for cn, cmask in enumerate(self.rois): savefile = os.path.split(self.imgpath) savefile = os.path.join(savefile[0], cmask + '_Mask.npy') #import pdb; pdb.set_trace() if os.path.isfile(savefile): index = np.load(savefile) self.Mask = np.zeros([self.img.shape[1], self.img.shape[2]]) if len(index) != 0: self.Mask[(index[0], index[1])] = 1 contours = skimage.measure.find_contours(self.Mask, 0.8) for n, contour in enumerate(contours): self.plottedlines.append( self.ax.plot(contour[:, 1], contour[:, 0], linewidth=2, color=self.colors[cn])) #msk.set_data(array) self.ax.axis('on') self.ax.set(adjustable="datalim") self.ax.set_ylim([self.img.shape[2], 0]) self.ax.set_xlim([0, self.img.shape[1]]) #self.imagehandle.ylim([0, 512]) #self.ax.xlim([0, 512]) #self.ax.set(adjustable = 'datalim') self.figure.canvas.draw_idle() def LoadImage(self): self.imgpath = self.filelist[self.currentImage] #self.cimg +=1 print('loading Image') print(self.imgpath) self.img = tifffile.imread(self.imgpath) print('Finished Loading Image') #self.img=exposure.adjust_gamma(self.img, 0.6) self.scroll.setMaximum( self.img.shape[0]) #set maximum number of images for scroll bar # create an axis self.ax = self.figure.add_axes([0, 0, 1, 1]) self.ax.axis('off') # discards the old graph #self.ax.hold(False) # plot data self.currentSlice = 0 #self.imagehandle = self.ax.imshow(np.std(self.img, axis=0), extent = [0, self.img.shape[1], 0, self.img.shape[2]]) #use extent to try to prevent resizing after plotting mask but only prevents resizing in Y #self.imagehandle = self.ax.imshow(np.std(self.img, axis=0)) self.imagehandle = self.ax.imshow(np.std(self.img, axis=0), aspect='equal', cmap='Greys_r') self.ax.set(adjustable="datalim") print('Xaxis_ ' + str(self.ax.get_xlim())) print('Yaxis_ ' + str(self.ax.get_ylim())) if 'text1' in dir(self): self.text1.set_text(self.imgpath[-50:]) else: self.text1 = self.ax.text(self.ax.get_xlim()[0] + 10, self.ax.get_ylim()[1] + 10, self.imgpath[-50:], color=(1, 1, 1), verticalalignment='bottom', horizontalalignment='left') #self.ax.text(250, 250, self.imgpath[-15], color = (1,1,1), verticalalignment = 'bottom', horizontalalignment ='right') #draw in masks self.drawROI() self.canvas.draw() def updateImage(self): print('updatingimage') #import pdb; pdb.set_trace() if self.radioImageType['Stack'].isChecked(): self.imagehandle.set_data(self.img[self.currentSlice, :, :]) self.imagehandle.set_clim(np.min(self.img), np.max(self.img)) elif self.radioImageType['StDev'].isChecked(): self.imagehandle.set_data(np.std(self.img, axis=0)) self.imagehandle.set_clim(np.min(np.std(self.img, axis=0)), np.max(np.std(self.img, axis=0))) elif self.radioImageType['Max'].isChecked(): self.imagehandle.set_data(np.max(self.img, axis=0)) self.imagehandle.set_clim(np.min(self.img), np.max(self.img)) elif self.radioImageType['Sum'].isChecked(): self.imagehandle.set_data(np.sum(self.img, axis=0)) self.imagehandle.set_clim(np.min(np.sum(self.img, axis=0)), np.max(np.sum(self.img, axis=0))) self.canvas.draw() def changeSlice(self): self.currentSlice = self.scroll.value() self.updateImage() def prevImage(self): self.currentImage -= 1 if self.currentImage >= len(self.filelist): self.currentImage = len(self.filelist) - 1 self.LoadImage() def nextImage(self): self.currentImage += 1 if self.currentImage < 0: self.currentImage = 0 self.LoadImage() def contrast(self): print('Not finished') self.cid1 = self.canvas.mpl_connect('button_press_event', self.onPressCanvas)
class ROISelect(QMdiSubWindow): ########### # Signals # ########### plotSignal = pyqtSignal() ######################## # Initializing Methods # ######################## def __init__(self, title, imgfile, control, parent=None): ''' Initializes internal variables ''' QMdiSubWindow.__init__(self, parent) self.setWindowTitle(title) self.imgfile = imgfile self.control = control self.xys = [] self.ROI = np.zeros((600,800)).astype(np.bool) for i in range(800): for j in range(600): self.xys.append((i,j)) self.create_main_frame() self.onDraw() def create_main_frame(self): ''' Creates the main window ''' # Widgets self.main_frame = QWidget() self.fig = Figure() self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.main_frame) self.axes = self.fig.add_subplot(111) self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame) self.reset = QPushButton('&Reset') # Connections self.cid = self.canvas.mpl_connect('button_press_event', self.onpress) self.control.imageChanged.connect(self.onImageChanged) self.control.closeSignal.connect(self.close) self.connect(self.reset, SIGNAL('clicked()'), self.onReset) # Layouts vbox = QVBoxLayout() vbox.addWidget(self.canvas) vbox.addWidget(self.mpl_toolbar) vbox.addWidget(self.reset) self.main_frame.setLayout(vbox) self.setWidget(self.main_frame) ######### # Slots # ######### def onReset(self): self.ROI = np.zeros((600,800)).astype(np.bool) self.cid = self.canvas.mpl_connect('button_press_event', self.onpress) self.onDraw() def onDraw(self): self.axes.clear() img = Image.open(self.imgfile) img = np.asarray(img) self.axes.imshow(img) self.axes.imshow(self.ROI, alpha=0.1, cmap='gray') self.plotSignal.emit() self.canvas.draw() def getROI(self, verts): ind = points_inside_poly(self.xys, verts) self.canvas.draw_idle() self.canvas.widgetlock.release(self.lasso) del self.lasso self.ROI = ind self.ROI = self.ROI.reshape((600,800), order='F') self.canvas.mpl_disconnect(self.cid) self.onDraw() def onpress(self, event): if self.canvas.widgetlock.locked(): return if event.inaxes is None: return self.lasso = Lasso(event.inaxes, (event.xdata, event.ydata), self.getROI) self.canvas.widgetlock(self.lasso) def onImageChanged(self, filename): ''' Catches the signal from the ControlPanel that the current image has changed ''' self.imgfile = str(filename) self.onDraw()
class Plotter(QMainWindow): ''' MainWindow consisting of menu bar, tab widget, tree list menu bar: load data tab widget: - dragWindow - widget consisting of picture viewer and two plots (raw and rms) tree list: to show loaded data, expand to see their channels and click to plot ''' def __init__(self, parent=None): super(Plotter, self).__init__(parent) self.ui = Ui_MainWindow() self.ui.setupUi(self) self.setCanvas() w = DragWindow() grid = QGridLayout() grid.addWidget(w) self.ui.tab_2.acceptDrops() self.ui.tab_2.setLayout(grid) self.moveConnection = None self.currentParent = None self.datas = [] self.offsetRecording = 0 self.lines = [{}, {}] def setCanvas(self): ''' set widgets for a tab page - left side show image of current frame - right side show two plots: raw and rms - bot: button to edit triggers and slider to adjust video-emg offset ''' ######################## # set matplotlib plots # ######################## self.fig = Figure(dpi=70) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.ui.mainFrame) self.axes = self.fig.add_subplot(211) self.axesRMS = self.fig.add_subplot(212) self.mpl_toolbar = NavigationToolbar2(self.canvas, self.ui.mainFrame) self.canvas.mpl_connect('draw_event', self.onDraw) #################################### # add button to matplotlib toolbar # #################################### redb = QPushButton('Edit Triggers') redb.setCheckable(True) self.mpl_toolbar.addWidget(redb) redb.clicked[bool].connect(self.toggleEditMode) # container for current frame of video layout = pq.GraphicsLayoutWidget() vb = layout.addViewBox() vb.setAspectLocked(True) self.ri = pq.ImageItem() vb.addItem(self.ri) # layout to organize elements grid = QGridLayout() wrapper = QWidget() vbox = QVBoxLayout(wrapper) splitter = QSplitter() vbox.addWidget(self.canvas) vbox.addWidget(self.mpl_toolbar) wrapper.setLayout(vbox) splitter.addWidget(layout) splitter.addWidget(wrapper) grid.addWidget(splitter) self.ri.show() layout.show() self.ui.mainFrame.setLayout(grid) def loadData(self): ''' open dialog for choosing files to load ''' fileNames, ok1= QFileDialog.getOpenFileNames( self, self.tr("Open data"), plattform.fixpath("D:\Master\emg\Benedictus"), self.tr("Pickle Files (*.pk)")) if ok1: self.load(fileNames) def load(self, fileNames): ''' unpickles data and add elements to tree view in case loading takes a while, a progress bar shows current status ''' progress = QProgressDialog("Loading files...", "Abort Copy", 0, len(fileNames), self) progress.setWindowModality(Qt.WindowModal) i = 0 for fileName in fileNames: with open(fileName, 'rb') as ifile: tmp = pickle.load(ifile) if os.path.isfile(fileName[:-2] + "oni"): tmp = tmp + (fileName[:-2]+"oni",) if isinstance(tmp, pyHPF.pyHPF): self.datas.append(tmp) else: self.datas.append(extendedPyHPF(tmp)) self.updateTree(fileName[-5:]) i+=1 progress.setValue(i) def updateTree(self, name): ''' adds DataItem to tree view ''' dataItem = QTreeWidgetItem(self.ui.dataView) dataItem.setText(0, name) dataItem.setFlags(Qt.ItemIsEnabled|Qt.ItemIsEditable) for i in range(len(self.datas[-1].data)/4): x = DataItem(dataItem) x.setText(0, self.datas[-1].name[i*4]) x.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) x.setNumData(self.datas[-1].data[i*4]) if hasattr(self.datas[-1], 'triggers'): x.triggers = self.datas[-1].triggers if hasattr(self.datas[-1], 'frames'): x.depth = self.datas[-1].seekFrame def toggleEditMode(self, toggle): ''' button pressed to toggle edit mode for triggers - if pressed in edit mode again, dialog to save changes to a existing file ''' if toggle: item = self.ui.dataView.selectedItems()[-1] self.interactionMode = Interactor(self.canvas, [self.axes, self.axesRMS], item.triggers, self.lines, item.emg.shape[0]) else: self.interactionMode = None reply = QMessageBox.question(self, 'QMessageBox.question()', 'Update triggers?', QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: fileName, ok= QFileDialog.getOpenFileName( self, self.tr("Choose which file to overwrite data"), plattform.fixpath("D:\Master\emg\Benedictus"), self.tr("Pickle Files (*.pk)")) if ok: save(self.ui.dataView.selectedItems()[-1].triggers, fileName) def plotData(self): ''' clicking a DataItem will plot the raw data and rms of emg data - if oni files are present, it will connect the mouse position inside plots to frame number of video ''' ################# # clearing # ################# self.axes.clear() self.axesRMS.clear() if self.moveConnection is not None: self.canvas.mpl_disconnect(self.moveConnection) ######################## # get selected item # ######################## if len(self.ui.dataView.selectedItems()) > 0: item = self.ui.dataView.selectedItems()[-1] if hasattr(item, 'depth'): self.ui.offsetSlider.setEnabled(True) ''' if selected channel is from other dataset - slider is available - current frame of video is set - and new parent is saved ''' if item.parent() is not self.currentParent: self.offsetRecording = 0 self.ui.offsetSlider.setValue(150) if self.currentParent: self.currentParent.child(0).depth(-1) self.ri.setImage(item.depth(0)) self.currentParent = item.parent() ######################## # if deselected, reset # ######################## else: self.ui.offsetSlider.setEnabled(False) self.ri.setImage(np.zeros((1,1,1))) ########################## # plot selected data # ########################## for item in self.ui.dataView.selectedItems(): c = int(item.text(0)[-1])-1 if c >= len(colors): c = c % len(colors) self.axes.plot(item.emg, label=item.text(0), c=colors[c]) self.axesRMS.plot(feature.rootMeanSquare(np.abs(item.emg), 200), c=colors[c]) self.lines = [{}, {}] for p in item.triggers: self.lines[0][p], = self.axes.plot([p]*2, [np.min(item.emg), np.max(item.emg)], c='k', animated = True) self.lines[1][p], = self.axesRMS.plot([p]*2, [np.min(item.emg), np.max(item.emg)], c='k', animated = True) c+=1 # connect mouse position inside plots to frame index of video self.moveConnection = self.canvas.mpl_connect("motion_notify_event", self.updateVideo) self.axes.legend() self.canvas.draw() def onDraw(self, event): ''' partially updating plots see: http://wiki.scipy.org/Cookbook/Matplotlib/Animations#head-3d51654b8306b1585664e7fe060a60fc76e5aa08 ''' items = self.ui.dataView.selectedItems() if len(items)== 0: return for i in range(2): ax = [self.axes, self.axesRMS] for t in items[-1].triggers: ax[i].draw_artist(self.lines[i][t]) self.canvas.blit() def correctOffset(self, offs): self.offsetRecording = offs-150 def updateVideo(self, event): ''' set frame index according to mouse position ''' if event.xdata is not None: frameIdx = int(event.xdata/2000.0*30 + self.offsetRecording) if frameIdx < 0: frameIdx = 0 if len(self.ui.dataView.selectedItems()) > 0: if hasattr(self.ui.dataView.selectedItems()[-1], 'depth'): self.ri.setImage(self.ui.dataView.selectedItems()[-1].depth(frameIdx))
class Main(QMainWindow, Ui_MainWindow): def __init__(self, ): super(Main, self).__init__() self.setupUi(self) self.fig_dict = {} self.fg_dict = {} self.mplfigs.itemClicked.connect(self.changefig) self.datacube.clicked.connect(self.selectFile) self.polarization.clicked.connect(self.selectPol) self.fetch.clicked.connect(self._fetch) self.res.clicked.connect(self._res) self.calc.clicked.connect(self._calc) self.reset.clicked.connect(self._reset) self.xst.valueChanged[str].connect(self.xchg) self.yst.valueChanged[str].connect(self.ychg) self.x_st, self.y_st = 0,0 fig = Figure() self.fig = fig self.addmpl(fig, np.ones((100,100))) self.previous_point = [] self.tempxpt,self.tempypt = [],[] self.allxpoints = [] self.allypoints = [] self.start_point = [] self.end_point = [] self.line = None self.roicolor = 'r' self.ax = plt.gca() self.dcube_path = '/Users/Mipanox/Desktop/coding_temp/SF/wzError/L1455_rgd.fits' self.poldt_path = '/Users/Mipanox/Desktop/coding_temp/SF/wzError/scupollegacy_l1455_cube.fits' self.dc_nm.setText(self.dcube_path) self.po_nm.setText(self.poldt_path) self.dn = None self.ds = None self.__ID2 = self.fig.canvas.mpl_connect( 'button_press_event', self.__button_press_callback) def selectFile(self): self.dc_nm.setText(QFileDialog.getOpenFileName()) self.dcube_path = unicode(self.dc_nm.text()) def selectPol(self): self.po_nm.setText(QFileDialog.getOpenFileName()) self.poldt_path = unicode(self.po_nm.text()) def xchg(self): self.x_st = self.xst.value() def ychg(self): self.y_st = self.yst.value() def _fetch(self): if self.checkBox.isChecked() == True: self.__fet() else: self.dn = sfn(ds=self.dcube_path,name='foo',od=2.,bn=1., pol=self.poldt_path,du=1.5e-5) self.ds = sf(ds=self.dcube_path,name='foo',od=2.,bn=1., pol=self.poldt_path,du=1.5e-5) m0 = self.dn.m0 m1 = self.dn.m1 i = 0 for n in [m0,m1]: fig_ = Figure() axf_ = fig_.add_subplot(111) cax = axf_.imshow(n,origin='lower') fig_.colorbar(cax) name = 'moment %s' %i self.fig_dict[name] = fig_ self.fg_dict[name] = n self.mplfigs.addItem(name) i += 1 def __fet(self): from astropy.utils.data import get_readable_fileobj from astropy.io import fits with get_readable_fileobj(self.dcube_path, cache=True) as f: fitsfile = fits.open(f) gd = fitsfile[0].data[0][0] dshd = fitsfile[0].header with get_readable_fileobj(self.poldt_path, cache=True) as e: fitsfile = fits.open(e) po = fitsfile[0].data[0][0] + 90. # to B-field pshd = fitsfile[0].header tx = 'gd-pol' fig = Figure() self.fig_dict[tx] = fig self.fg_dict[tx] = [gd,po] self.mplfigs.addItem(tx) axf = fig.add_subplot(111) self.__quiver(gd,po,axf,tx) def __quiver(self,gd,po,plt,tx): lx,ly = gd.shape y,x = np.mgrid[0:(lx-1):(lx)*1j, 0:(ly-1):(ly)*1j] gx,gy = -np.sin(np.radians(gd)),np.cos(np.radians(gd)) px,py = -np.sin(np.radians(po)+90.),np.cos(np.radians(po)+90.) quiveropts = dict(headlength=0, pivot='middle', scale=5e1, headaxislength=0) plt.axis('equal'); plt.quiver(x,y,gx,gy,color='r',**quiveropts) plt.quiver(x,y,px,py,color='b',alpha=0.5,**quiveropts) def _calc(self): def avg_adj(ar,n): # reshaping even-indexed arrays (M,N) = ar.shape tt = np.zeros((M-n+1,N-n+1)) for (x,y),i in np.ndenumerate(ar): if x > ar.shape[0]-n or y > ar.shape[1]-n: continue else: ap = ar[slice(x,x+n),slice(y,y+n)] tt[x,y] = ap.mean() return tt pol = self.dn._grad(pol=1)[0] grd = self.ds._grad() for i in range(1,len(grd)): if i % 2: pt = avg_adj(pol,i+1) else: pt = pol[i/2:-i/2,i/2:-i/2] gt = grd[i] tx = '%s x %s' %(i+1,i+1) fig = Figure() self.fig_dict[tx] = fig self.fg_dict[tx] = [gt,pt] self.mplfigs.addItem(tx) axf = fig.add_subplot(111) self.__quiver(gt,pt,axf,tx) def _res(self, item): cg,cp = self.fg[0],self.fg[1] if self.x_st >= 0 and self.y_st >= 0: gd = np.pad(cg,((0,2*self.x_st),(0,2*self.y_st)), mode='constant', constant_values=(np.nan)) po = np.pad(cp,((self.x_st,self.x_st),(self.y_st,self.y_st)), mode='constant', constant_values=(np.nan)) elif self.x_st < 0 and self.y_st >= 0: gd = np.pad(cg,((-self.x_st,-self.x_st),(0,2*self.y_st)), mode='constant', constant_values=(np.nan)) po = np.pad(cp,((0,-2*self.x_st),(self.y_st,self.y_st)), mode='constant', constant_values=(np.nan)) elif self.x_st >=0 and self.y_st < 0: gd = np.pad(cg,((0,2*self.x_st),(-self.y_st,-self.y_st)), mode='constant', constant_values=(np.nan)) po = np.pad(cp,((self.x_st,self.x_st),(0,-2*self.y_st)), mode='constant', constant_values=(np.nan)) else: gd = np.pad(cg,((-self.x_st,-self.x_st),(-self.y_st,-self.y_st)), mode='constant', constant_values=(np.nan)) po = np.pad(cp,((0,-2*self.x_st),(0,-2*self.y_st)), mode='constant', constant_values=(np.nan)) if self.x_st == 0 and self.y_st == 0: pass else: tx = '%s - (%s x %s) shifted' %(str(self.mplfigs.currentItem().text()), self.x_st,self.y_st) fig = Figure() self.fig_dict[tx] = fig self.fg_dict[tx] = [gd,po] self.mplfigs.addItem(tx) axf = fig.add_subplot(111) self.__quiver(gd,po,axf,tx) if len(self.allxpoints) > 0: # if roi selected if self.checkBox.isChecked() == True: tp = self.getMask(gd) else: self.tempxpt,self.tempypt = self.allxpoints,self.allypoints old_xd,old_yd = self.dn.m0.shape new_xd,new_yd = gd.shape for i in range(len(self.allxpoints)): self.allxpoints[i] *= float(new_xd) / float(old_xd) self.allypoints[i] *= float(new_yd) / float(old_yd) tp = self.getMask(gd) gd[tp==False] = np.nan po[tp==False] = np.nan gx,gy = -np.sin(np.radians(gd)),np.cos(np.radians(gd)) px,py = -np.sin(np.radians(po)),np.cos(np.radians(po)) # +/- 90 doesn't matter overlay = np.sum(~np.isnan(gd) * ~np.isnan(po) * 1.) self.spsize.setText('%d' %(overlay)) v_c = vc(v1=np.array([gx,gy]),v2=np.array([px,py])) self.rho_c.setText('%3e' %(v_c.corr_c()) ) self.rho_h.setText('%3e' %(v_c.corr_h()) ) self.allxpoints = self.tempxpt self.allypoints = self.tempypt def _reset(self): self.previous_point = [] self.tempxpt,self.tempypt = [],[] self.allxpoints = [] self.allypoints = [] self.start_point = [] self.end_point = [] self.line = None ## remember to right-click before reset if changed frame self.ax.lines = [] self.changefig(self.mplfigs.currentItem()) self.xst.setValue(0) self.yst.setValue(0) def changefig(self, item): text = str(item.text()) self.rmmpl() self.addmpl(self.fig_dict[text], self.fg_dict[text]) def addmpl(self, fig, fg): self.canvas = FigureCanvas(fig) self.mplvl.addWidget(self.canvas) self.toolbar = NavigationToolbar(self.canvas, self.mplwindow, coordinates=True) self.mplvl.addWidget(self.toolbar) self.fg = fg self.ax = self.fig.add_subplot(111) self.canvas.draw() self.canvas.setFocusPolicy( Qt.ClickFocus ) self.canvas.setFocus() self.canvas.mpl_connect('button_press_event', self.__button_press_callback) def rmmpl(self,): self.mplvl.removeWidget(self.canvas) self.canvas.close() self.mplvl.removeWidget(self.toolbar) self.toolbar.close() def __button_press_callback(self, event): if event.inaxes: x, y = event.xdata, event.ydata self.ax = event.inaxes if event.button == 1 and event.dblclick == False: # If you press the left button, single click if self.line == None: # if there is no line, create a line self.line = plt.Line2D([x, x], [y, y], marker='o', color=self.roicolor) self.start_point = [x,y] self.previous_point = self.start_point self.allxpoints=[x] self.allypoints=[y] self.ax.add_line(self.line) self.canvas.draw() # add a segment else: # if there is a line, create a segment self.line = plt.Line2D([self.previous_point[0], x], [self.previous_point[1], y], marker = 'o',color=self.roicolor) self.previous_point = [x,y] self.allxpoints.append(x) self.allypoints.append(y) event.inaxes.add_line(self.line) self.canvas.draw() elif ((event.button == 1 and event.dblclick==True) or (event.button == 3 and event.dblclick==False)) and self.line != None: # close the loop and disconnect self.canvas.mpl_disconnect(self.__ID2) #joerg self.line.set_data([self.previous_point[0], self.start_point[0]], [self.previous_point[1], self.start_point[1]]) self.ax.add_line(self.line) self.canvas.draw() self.line = None def getMask(self, ci): ny, nx = ci.shape poly_verts = [(self.allxpoints[0], self.allypoints[0])] for i in range(len(self.allxpoints)-1, -1, -1): poly_verts.append((self.allxpoints[i], self.allypoints[i])) x, y = np.meshgrid(np.arange(nx), np.arange(ny)) x, y = x.flatten(), y.flatten() points = np.vstack((x,y)).T ROIpath = mplPath.Path(poly_verts) grid = ROIpath.contains_points(points).reshape((ny,nx)) return grid
class ProfileDockWidget(QDockWidget): """ DockWidget class to display the profile """ closeSignal = pyqtSignal() def __init__(self, iface, geometry, mntButton=False, zerosButton=False): """ Constructor :param iface: interface :param width: dock widget geometry """ QDockWidget.__init__(self) self.setWindowTitle(QCoreApplication.translate("VDLTools", "Profile Tool")) self.__iface = iface self.__geom = geometry self.__canvas = self.__iface.mapCanvas() self.__types = ['PDF', 'PNG'] # ], 'SVG', 'PS'] self.__libs = [] if Qwt5_loaded: self.__lib = 'Qwt5' self.__libs.append('Qwt5') if matplotlib_loaded: self.__libs.append('Matplotlib') elif matplotlib_loaded: self.__lib = 'Matplotlib' self.__libs.append('Matplotlib') else: self.__lib = None self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "No graph lib available (qwt5 or matplotlib)"), level=QgsMessageBar.CRITICAL, duration=0) self.__doTracking = False self.__vline = None self.__profiles = None self.__numLines = None self.__mntPoints = None self.__marker = None self.__tabmouseevent = None if self.__geom is not None: self.setGeometry(self.__geom) self.__contentWidget = QWidget() self.setWidget(self.__contentWidget) self.__boxLayout = QHBoxLayout() self.__contentWidget.setLayout(self.__boxLayout) self.__plotFrame = QFrame() self.__frameLayout = QHBoxLayout() self.__plotFrame.setLayout(self.__frameLayout) self.__printLayout = QHBoxLayout() self.__printLayout.addWidget(self.__plotFrame) self.__legendLayout = QVBoxLayout() self.__printLayout.addLayout(self.__legendLayout) self.__printWdg = QWidget() self.__printWdg.setLayout(self.__printLayout) self.__plotWdg = None self.__changePlotWidget() size = QSize(150, 20) self.__boxLayout.addWidget(self.__printWdg) self.__vertLayout = QVBoxLayout() self.__libCombo = QComboBox() self.__libCombo.setFixedSize(size) self.__libCombo.addItems(self.__libs) self.__vertLayout.addWidget(self.__libCombo) self.__libCombo.currentIndexChanged.connect(self.__setLib) if mntButton: self.__displayMnt = False self.__mntButton = QPushButton(QCoreApplication.translate("VDLTools", "Display MNT")) self.__mntButton.setFixedSize(size) self.__mntButton.clicked.connect(self.__mnt) self.__vertLayout.addWidget(self.__mntButton) if zerosButton: self.__displayZeros = False self.__zerosButton = QPushButton(QCoreApplication.translate("VDLTools", "Display Zeros")) self.__zerosButton.setFixedSize(size) self.__zerosButton.clicked.connect(self.__zeros) self.__vertLayout.addWidget(self.__zerosButton) else: self.__displayZeros = True self.__maxLabel = QLabel("y max") self.__maxLabel.setFixedSize(size) self.__vertLayout.addWidget(self.__maxLabel) self.__maxSpin = QSpinBox() self.__maxSpin.setFixedSize(size) self.__maxSpin.setRange(-10000, 10000) self.__maxSpin.valueChanged.connect(self.__reScalePlot) self.__vertLayout.addWidget(self.__maxSpin) self.__vertLayout.insertSpacing(10, 20) self.__minLabel = QLabel("y min") self.__minLabel.setFixedSize(size) self.__vertLayout.addWidget(self.__minLabel) self.__minSpin = QSpinBox() self.__minSpin.setFixedSize(size) self.__minSpin.setRange(-10000, 10000) self.__minSpin.valueChanged.connect(self.__reScalePlot) self.__vertLayout.addWidget(self.__minSpin) self.__vertLayout.insertSpacing(10, 40) self.__typeCombo = QComboBox() self.__typeCombo.setFixedSize(size) self.__typeCombo.addItems(self.__types) self.__vertLayout.addWidget(self.__typeCombo) self.__saveButton = QPushButton(QCoreApplication.translate("VDLTools", "Save")) self.__saveButton.setFixedSize(size) self.__saveButton.clicked.connect(self.__save) self.__vertLayout.addWidget(self.__saveButton) self.__boxLayout.addLayout(self.__vertLayout) self.__maxSpin.setEnabled(False) self.__minSpin.setEnabled(False) self.__colors = [] for cn in QColor.colorNames(): qc = QColor(cn) val = qc.red() + qc.green() + qc.blue() if 0 < val < 450: self.__colors.append(cn) def mntButton(self): """ To get the mnt button instance :return: mnt button instance """ return self.__mntButton def zerosButton(self): """ To get the zeros button instance :return: zeros button instance """ return self.__zerosButton def displayMnt(self): """ To get if we want to display mnt :return: true or false """ return self.__displayMnt def __mnt(self): """ To toggle mnt display choice """ if self.__displayMnt: self.__displayMnt = False self.__mntButton.setText(QCoreApplication.translate("VDLTools", "Display MNT")) else: self.__displayMnt = True self.__mntButton.setText(QCoreApplication.translate("VDLTools", "Remove MNT")) def __zeros(self): """ To toggle if we want to display zero elevations or not """ if self.__displayZeros: self.__displayZeros = False self.__zerosButton.setText(QCoreApplication.translate("VDLTools", "Display Zeros")) else: self.__displayZeros = True self.__zerosButton.setText(QCoreApplication.translate("VDLTools", "Remove Zeros")) def __changePlotWidget(self): """ When plot widget is change (qwt <-> matplotlib) """ self.__activateMouseTracking(False) while self.__frameLayout.count(): child = self.__frameLayout.takeAt(0) child.widget().deleteLater() self.__plotWdg = None if self.__lib == 'Qwt5': self.__plotWdg = QwtPlot(self.__plotFrame) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(10) sizePolicy.setVerticalStretch(0) self.__plotWdg.setSizePolicy(sizePolicy) self.__plotWdg.setAutoFillBackground(False) # Decoration self.__plotWdg.setCanvasBackground(Qt.white) self.__plotWdg.plotLayout().setAlignCanvasToScales(False) self.__plotWdg.plotLayout().setSpacing(100) self.__plotWdg.plotLayout().setCanvasMargin(10, QwtPlot.xBottom) self.__plotWdg.plotLayout().setCanvasMargin(10, QwtPlot.yLeft) title = QwtText(QCoreApplication.translate("VDLTools", "Distance [m]")) title.setFont(QFont("Helvetica", 10)) self.__plotWdg.setAxisTitle(QwtPlot.xBottom, title) title.setText(QCoreApplication.translate("VDLTools", "Elevation [m]")) title.setFont(QFont("Helvetica", 10)) self.__plotWdg.setAxisTitle(QwtPlot.yLeft, title) self.__zoomer = QwtPlotZoomer(QwtPlot.xBottom, QwtPlot.yLeft, QwtPicker.DragSelection, QwtPicker.AlwaysOff, self.__plotWdg.canvas()) self.__zoomer.setRubberBandPen(QPen(Qt.blue)) grid = QwtPlotGrid() grid.setPen(QPen(QColor('grey'), 0, Qt.DotLine)) grid.attach(self.__plotWdg) self.__frameLayout.addWidget(self.__plotWdg) elif self.__lib == 'Matplotlib': fig = Figure((1.0, 1.0), linewidth=0.0, subplotpars=SubplotParams(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)) font = {'family': 'arial', 'weight': 'normal', 'size': 12} rc('font', **font) rect = fig.patch rect.set_facecolor((0.9, 0.9, 0.9)) self.__axes = fig.add_axes((0.07, 0.16, 0.92, 0.82)) self.__axes.set_xbound(0, 1000) self.__axes.set_ybound(0, 1000) self.__manageMatplotlibAxe(self.__axes) self.__plotWdg = FigureCanvasQTAgg(fig) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) self.__plotWdg.setSizePolicy(sizePolicy) self.__frameLayout.addWidget(self.__plotWdg) def setProfiles(self, profiles, numLines): """ To set the profiles :param profiles: profiles : positions with elevations (for line and points) :param numLines: number of selected connected lines """ self.__numLines = numLines self.__profiles = profiles if self.__lib == 'Matplotlib': self.__prepare_points() def __getLinearPoints(self): """ To extract the linear points of the profile """ profileLen = 0 self.__profiles[0]['l'] = profileLen for i in range(0, len(self.__profiles)-1): x1 = float(self.__profiles[i]['x']) y1 = float(self.__profiles[i]['y']) x2 = float(self.__profiles[i+1]['x']) y2 = float(self.__profiles[i+1]['y']) profileLen += sqrt(((x2-x1)*(x2-x1)) + ((y2-y1)*(y2-y1))) self.__profiles[i+1]['l'] = profileLen def __getMnt(self, settings): """ To get the MN data for the profile :param settings: settings containing MN url """ if settings is None or settings.mntUrl is None or settings.mntUrl == "None": url = 'https://map.lausanne.ch/prod/wsgi/profile.json' elif settings.mntUrl == "": return else: url = settings.mntUrl names = ['MNT', 'MNS', 'Rocher (approx.)'] data = "layers=MNT%2CMNS%2CRocher%20(approx.)&geom=%7B%22type%22%3A%22LineString%22%2C%22coordinates%22%3A%5B" pos = 0 for i in range(len(self.__profiles)): if pos > 0: data += "%2C" pos += 1 data += "%5B" + str(self.__profiles[i]['x']) + "%2C" + str(self.__profiles[i]['y']) + "%5D" data += "%5D%7D&nbPoints=" + str(int(self.__profiles[len(self.__profiles)-1]['l']+1)) try: response = requests.post(url, data=data) j = response.text j_obj = json.loads(j) profile = j_obj['profile'] self.__mntPoints = [] self.__mntPoints.append(names) mnt_l = [] mnt_z = [] for p in range(len(names)): z = [] mnt_z.append(z) for pt in profile: mnt_l.append(float(pt['dist'])) values = pt['values'] for p in range(len(names)): if names[p] in values: mnt_z[p].append(float(values[names[p]])) else: mnt_z[p].append(None) self.__mntPoints.append(mnt_l) self.__mntPoints.append(mnt_z) except HTTPError as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "HTTP Error"), QCoreApplication.translate("VDLTools", "status error") + "[" + str(e.code) + "] : " + e.reason, level=QgsMessageBar.CRITICAL, duration=0) except URLError as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "URL Error"), e.reason, level=QgsMessageBar.CRITICAL, duration=0) except ValueError as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "No MNT values here"), level=QgsMessageBar.CRITICAL, duration=0) def attachCurves(self, names, settings, usedMnts): """ To attach the curves for the layers to the profile :param names: layers names """ if (self.__profiles is None) or (self.__profiles == 0): return self.__getLinearPoints() if usedMnts is not None and (usedMnts[0] or usedMnts[1] or usedMnts[2]): self.__getMnt(settings) c = 0 if self.__mntPoints is not None: for p in range(len(self.__mntPoints[0])): if usedMnts[p]: legend = QLabel("<font color='" + self.__colors[c] + "'>" + self.__mntPoints[0][p] + "</font>") self.__legendLayout.addWidget(legend) if self.__lib == 'Qwt5': xx = [list(g) for k, g in itertools.groupby(self.__mntPoints[1], lambda x: x is None) if not k] yy = [list(g) for k, g in itertools.groupby(self.__mntPoints[2][p], lambda x: x is None) if not k] for j in range(len(xx)): curve = QwtPlotCurve(self.__mntPoints[0][p]) curve.setData(xx[j], yy[j]) curve.setPen(QPen(QColor(self.__colors[c]), 3)) curve.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': qcol = QColor(self.__colors[c]) self.__plotWdg.figure.get_axes()[0].plot(self.__mntPoints[1], self.__mntPoints[2][p], gid=self.__mntPoints[0][p], linewidth=3) tmp = self.__plotWdg.figure.get_axes()[0].get_lines() for t in range(len(tmp)): if self.__mntPoints[0][p] == tmp[t].get_gid(): tmp[c].set_color((old_div(qcol.red(), 255.0), old_div(qcol.green(), 255.0), old_div(qcol.blue(), 255.0), old_div(qcol.alpha(), 255.0))) self.__plotWdg.draw() break c += 1 if 'z' in self.__profiles[0]: for i in range(len(self.__profiles[0]['z'])): if i < self.__numLines: v = 0 else: v = i - self.__numLines + 1 name = names[v] xx = [] yy = [] for prof in self.__profiles: xx.append(prof['l']) yy.append(prof['z'][i]) for j in range(len(yy)): if yy[j] is None: xx[j] = None if i == 0 or i > (self.__numLines-1): legend = QLabel("<font color='" + self.__colors[c] + "'>" + name + "</font>") self.__legendLayout.addWidget(legend) if self.__lib == 'Qwt5': # Split xx and yy into single lines at None values xx = [list(g) for k, g in itertools.groupby(xx, lambda x: x is None) if not k] yy = [list(g) for k, g in itertools.groupby(yy, lambda x: x is None) if not k] # Create & attach one QwtPlotCurve per one single line for j in range(len(xx)): curve = QwtPlotCurve(name) curve.setData(xx[j], yy[j]) curve.setPen(QPen(QColor(self.__colors[c]), 3)) if i > (self.__numLines-1): curve.setStyle(QwtPlotCurve.Dots) pen = QPen(QColor(self.__colors[c]), 8) pen.setCapStyle(Qt.RoundCap) curve.setPen(pen) curve.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': qcol = QColor(self.__colors[c]) if i < self.__numLines: self.__plotWdg.figure.get_axes()[0].plot(xx, yy, gid=name, linewidth=3) else: self.__plotWdg.figure.get_axes()[0].plot(xx, yy, gid=name, linewidth=5, marker='o', linestyle='None') tmp = self.__plotWdg.figure.get_axes()[0].get_lines() for t in range(len(tmp)): if name == tmp[t].get_gid(): tmp[c].set_color((old_div(qcol.red(), 255.0), old_div(qcol.green(), 255.0), old_div(qcol.blue(), 255.0), old_div(qcol.alpha(), 255.0))) self.__plotWdg.draw() break c += 1 # scaling this try: self.__reScalePlot(None, True) except: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Rescale problem... (trace printed)"), level=QgsMessageBar.CRITICAL, duration=0) print(sys.exc_info()[0], traceback.format_exc()) if self.__lib == 'Qwt5': self.__plotWdg.replot() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].redraw_in_frame() self.__plotWdg.draw() self.__activateMouseTracking(True) self.__marker.show() def __reScalePlot(self, value=None, auto=False): """ To rescale the profile plot depending to the bounds :param value: juste because connections give value :param auto: if automatic ranges calcul is wanted """ if (self.__profiles is None) or (self.__profiles == 0): self.__plotWdg.replot() return maxi = 0 for i in range(len(self.__profiles)): if (ceil(self.__profiles[i]['l'])) > maxi: maxi = ceil(self.__profiles[i]['l']) if self.__lib == 'Qwt5': self.__plotWdg.setAxisScale(2, 0, maxi, 0) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].set_xbound(0, maxi) minimumValue = self.__minSpin.value() maximumValue = self.__maxSpin.value() # to set max y and min y displayed if auto: minimumValue = 1000000000 maximumValue = -1000000000 for i in range(len(self.__profiles)): if 'z' in self.__profiles[i]: mini = self.__minTab(self.__profiles[i]['z']) if (mini > 0 or self.__displayZeros) and mini < minimumValue: minimumValue = ceil(mini) - 1 maxi = self.__maxTab(self.__profiles[i]['z']) if maxi > maximumValue: maximumValue = floor(maxi) + 1 if self.__mntPoints is not None: for pts in self.__mntPoints[2]: miniMnt = self.__minTab(pts) if (miniMnt > 0 or self.__displayZeros) and miniMnt < minimumValue: minimumValue = ceil(miniMnt) - 1 maxiMnt = self.__maxTab(pts) if maxiMnt > maximumValue: maximumValue = floor(maxiMnt) + 1 self.__maxSpin.setValue(maximumValue) self.__minSpin.setValue(minimumValue) self.__maxSpin.setEnabled(True) self.__minSpin.setEnabled(True) if self.__lib == 'Qwt5': rect = QRectF(0, minimumValue, maxi, maximumValue-minimumValue) self.__zoomer.setZoomBase(rect) # to draw vertical lines for i in range(len(self.__profiles)): zz = [] for j in range(self.__numLines): if self.__profiles[i]['z'][j] is not None: zz.append(j) color = None if len(zz) == 2: width = 3 color = QColor('red') else: width = 1 if self.__lib == 'Qwt5': vertLine = QwtPlotMarker() vertLine.setLineStyle(QwtPlotMarker.VLine) pen = vertLine.linePen() pen.setWidth(width) if color is not None: pen.setColor(color) vertLine.setLinePen(pen) vertLine.setXValue(self.__profiles[i]['l']) label = vertLine.label() label.setText(str(i)) vertLine.setLabel(label) vertLine.setLabelAlignment(Qt.AlignLeft) vertLine.attach(self.__plotWdg) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].vlines(self.__profiles[i]['l'], minimumValue, maximumValue, linewidth=width) if minimumValue < maximumValue: if self.__lib == 'Qwt5': self.__plotWdg.setAxisScale(0, minimumValue, maximumValue, 0) self.__plotWdg.replot() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].set_ybound(minimumValue, maximumValue) self.__plotWdg.figure.get_axes()[0].redraw_in_frame() self.__plotWdg.draw() @staticmethod def __minTab(tab): """ To get the minimum value in a table :param tab: table to scan :return: minimum value """ mini = 1000000000 for t in tab: if t is None: continue if t < mini: mini = t return mini @staticmethod def __maxTab(tab): """ To get the maximum value in a table :param tab: table to scan :return: maximum value """ maxi = -1000000000 for t in tab: if t is None: continue if t > maxi: maxi = t return maxi def __setLib(self): """ To set the new widget library (qwt <-> matplotlib) """ self.__lib = self.__libs[self.__libCombo.currentIndex()] self.__changePlotWidget() def __save(self): """ To save the profile in a file, on selected format """ idx = self.__typeCombo.currentIndex() if idx == 0: self.__outPDF() elif idx == 1: self.__outPNG() else: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Invalid index ") + str(idx), level=QgsMessageBar.CRITICAL, duration=0) def __outPDF(self): """ To save the profile as pdf file """ fileName = QFileDialog.getSaveFileName( self.__iface.mainWindow(), QCoreApplication.translate("VDLTools", "Save As"), QCoreApplication.translate("VDLTools", "Profile.pdf"),"Portable Document Format (*.pdf)") if fileName is not None: if self.__lib == 'Qwt5': printer = QPrinter() printer.setCreator(QCoreApplication.translate("VDLTools", "QGIS Profile Plugin")) printer.setOutputFileName(fileName) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOrientation(QPrinter.Landscape) self.__plotWdg.print_(printer) elif self.__lib == 'Matplotlib': self.__plotWdg.figure.savefig(str(fileName)) def __outPNG(self): """ To save the profile as png file """ fileName = QFileDialog.getSaveFileName( self.__iface.mainWindow(), QCoreApplication.translate("VDLTools", "Save As"), QCoreApplication.translate("VDLTools", "Profile.png"),"Portable Network Graphics (*.png)") if fileName is not None: QPixmap.grabWidget(self.__printWdg).save(fileName, "PNG") def clearData(self): """ To clear the displayed data """ if self.__profiles is None: return if self.__lib == 'Qwt5': self.__plotWdg.clear() self.__profiles = None temp1 = self.__plotWdg.itemList() for j in range(len(temp1)): if temp1[j].rtti() == QwtPlotItem.Rtti_PlotCurve: temp1[j].detach() elif self.__lib == 'Matplotlib': self.__plotWdg.figure.get_axes()[0].cla() self.__manageMatplotlibAxe(self.__plotWdg.figure.get_axes()[0]) self.__maxSpin.setEnabled(False) self.__minSpin.setEnabled(False) self.__maxSpin.setValue(0) self.__minSpin.setValue(0) # clear legend while self.__legendLayout.count(): child = self.__legendLayout.takeAt(0) child.widget().deleteLater() def __manageMatplotlibAxe(self, axe): """ To manage the axes for matplotlib library :param axe: the axes element """ axe.grid() axe.tick_params(axis="both", which="major", direction="out", length=10, width=1, bottom=True, top=False, left=True, right=False) axe.minorticks_on() axe.tick_params(axis="both", which="minor", direction="out", length=5, width=1, bottom=True, top=False, left=True, right=False) axe.set_xlabel(QCoreApplication.translate("VDLTools", "Distance [m]")) axe.set_ylabel(QCoreApplication.translate("VDLTools", "Elevation [m]")) def __activateMouseTracking(self, activate): """ To (de)activate the mouse tracking on the profile for matplotlib library :param activate: true to activate, false to deactivate """ if activate: self.__doTracking = True self.__loadRubber() self.cid = self.__plotWdg.mpl_connect('motion_notify_event', self.__mouseevent_mpl) elif self.__doTracking: self.__doTracking = False self.__plotWdg.mpl_disconnect(self.cid) if self.__marker is not None: self.__canvas.scene().removeItem(self.__marker) try: if self.__vline is not None: self.__plotWdg.figure.get_axes()[0].lines.remove(self.__vline) self.__plotWdg.draw() except Exception as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Tracking exception : ") + str(e), level=QgsMessageBar.CRITICAL, duration=0) def __mouseevent_mpl(self, event): """ To manage matplotlib mouse tracking event :param event: mouse tracking event """ if event.xdata is not None: try: if self.__vline is not None: self.__plotWdg.figure.get_axes()[0].lines.remove(self.__vline) except Exception as e: self.__iface.messageBar().pushMessage( QCoreApplication.translate("VDLTools", "Mouse event exception : ") + str(e), level=QgsMessageBar.CRITICAL, duration=0) xdata = float(event.xdata) self.__vline = self.__plotWdg.figure.get_axes()[0].axvline(xdata, linewidth=2, color='k') self.__plotWdg.draw() i = 1 while i < len(self.__tabmouseevent)-1 and xdata > self.__tabmouseevent[i][0]: i += 1 i -= 1 x = self.__tabmouseevent[i][1] + (self.__tabmouseevent[i + 1][1] - self.__tabmouseevent[i][1]) / ( self.__tabmouseevent[i + 1][0] - self.__tabmouseevent[i][0]) * (xdata - self.__tabmouseevent[i][0]) y = self.__tabmouseevent[i][2] + (self.__tabmouseevent[i + 1][2] - self.__tabmouseevent[i][2]) / ( self.__tabmouseevent[i + 1][0] - self.__tabmouseevent[i][0]) * (xdata - self.__tabmouseevent[i][0]) self.__marker.show() self.__marker.setCenter(QgsPoint(x, y)) def __loadRubber(self): """ To load te rubber band for mouse tracking on map """ self.__marker = QgsVertexMarker(self.__canvas) self.__marker.setIconSize(5) self.__marker.setIconType(QgsVertexMarker.ICON_BOX) self.__marker.setPenWidth(3) def __prepare_points(self): """ To prepare the points on map for mouse tracking on profile """ self.__tabmouseevent = [] length = 0 for i, point in enumerate(self.__profiles): if i == 0: self.__tabmouseevent.append([0, point['x'], point['y']]) else: length += ((self.__profiles[i]['x'] - self.__profiles[i-1]['x']) ** 2 + (self.__profiles[i]['y'] - self.__profiles[i-1]['y']) ** 2) ** 0.5 self.__tabmouseevent.append([float(length), float(point['x']), float(point['y'])]) def closeEvent(self, event): """ When the dock widget is closed :param event: close event """ if self.__maxSpin is not None: Signal.safelyDisconnect(self.__maxSpin.valueChanged, self.__reScalePlot) self.__maxSpin = None if self.__minSpin is not None: Signal.safelyDisconnect(self.__minSpin.valueChanged, self.__reScalePlot) self.__minSpin = None if self.__saveButton is not None: Signal.safelyDisconnect(self.__saveButton.clicked, self.__save) self.__saveButton = None if self.__libCombo is not None: Signal.safelyDisconnect(self.__libCombo.currentIndexChanged, self.__setLib) self.__libCombo = None self.closeSignal.emit() if self.__marker is not None: self.__marker.hide() QDockWidget.closeEvent(self, event)