diff -r c8e008b8cefe -r 72a826dfcfbb controls/DebugVariablePanel/DebugVariableGraphicViewer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py Wed Jul 31 10:45:07 2013 +0900 @@ -0,0 +1,1410 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +#This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor +#based on the plcopen standard. +# +#Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD +# +#See COPYING file for copyrights details. +# +#This library is free software; you can redistribute it and/or +#modify it under the terms of the GNU General Public +#License as published by the Free Software Foundation; either +#version 2.1 of the License, or (at your option) any later version. +# +#This library is distributed in the hope that it will be useful, +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +#General Public License for more details. +# +#You should have received a copy of the GNU General Public +#License along with this library; if not, write to the Free Software +#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from types import TupleType +from time import time as gettime +import numpy + +import wx + +import matplotlib +matplotlib.use('WX') +import matplotlib.pyplot +from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas +from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap +from matplotlib.backends.backend_agg import FigureCanvasAgg +from mpl_toolkits.mplot3d import Axes3D + +from editors.DebugViewer import REFRESH_PERIOD + +from DebugVariableItem import DebugVariableItem +from DebugVariableViewer import * +from GraphButton import GraphButton + +# Graph variable display type +GRAPH_PARALLEL, GRAPH_ORTHOGONAL = range(2) + +# Canvas height +[SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200] + +CANVAS_BORDER = (20., 10.) # Border height on at bottom and top of graph +CANVAS_PADDING = 8.5 # Border inside graph where no label is drawn +VALUE_LABEL_HEIGHT = 17. # Height of variable label in graph +AXES_LABEL_HEIGHT = 12.75 # Height of variable value in graph + +# Colors used cyclically for graph curves +COLOR_CYCLE = ['r', 'b', 'g', 'm', 'y', 'k'] +# Color for graph cursor +CURSOR_COLOR = '#800080' + +#------------------------------------------------------------------------------- +# Debug Variable Graphic Viewer Helpers +#------------------------------------------------------------------------------- + +def merge_ranges(ranges): + """ + Merge variables data range in a list to return a range of minimal min range + value and maximal max range value extended of 10% for keeping a padding + around graph in canvas + @param ranges: [(range_min_value, range_max_value),...] + @return: merged_range_min_value, merged_range_max_value + """ + # Get minimal and maximal range value + min_value = max_value = None + for range_min, range_max in ranges: + # Update minimal range value + if min_value is None: + min_value = range_min + elif range_min is not None: + min_value = min(min_value, range_min) + + # Update maximal range value + if max_value is None: + max_value = range_max + elif range_min is not None: + max_value = max(max_value, range_max) + + # Calculate range center and width if at least one valid range is defined + if min_value is not None and max_value is not None: + center = (min_value + max_value) / 2. + range_size = max(1.0, max_value - min_value) + + # Set default center and with if no valid range is defined + else: + center = 0.5 + range_size = 1.0 + + # Return range expended from 10 % + return center - range_size * 0.55, center + range_size * 0.55 + +#------------------------------------------------------------------------------- +# Debug Variable Graphic Viewer Drop Target +#------------------------------------------------------------------------------- + +""" +Class that implements a custom drop target class for Debug Variable Graphic +Viewer +""" + +class DebugVariableGraphicDropTarget(wx.TextDropTarget): + + def __init__(self, parent, window): + """ + Constructor + @param parent: Reference to Debug Variable Graphic Viewer + @param window: Reference to the Debug Variable Panel + """ + wx.TextDropTarget.__init__(self) + self.ParentControl = parent + self.ParentWindow = window + + def __del__(self): + """ + Destructor + """ + # Remove reference to Debug Variable Graphic Viewer and Debug Variable + # Panel + self.ParentControl = None + self.ParentWindow = None + + def OnDragOver(self, x, y, d): + """ + Function called when mouse is dragged over Drop Target + @param x: X coordinate of mouse pointer + @param y: Y coordinate of mouse pointer + @param d: Suggested default for return value + """ + # Signal parent that mouse is dragged over + self.ParentControl.OnMouseDragging(x, y) + + return wx.TextDropTarget.OnDragOver(self, x, y, d) + + def OnDropText(self, x, y, data): + """ + Function called when mouse is released in Drop Target + @param x: X coordinate of mouse pointer + @param y: Y coordinate of mouse pointer + @param data: Text associated to drag'n drop + """ + # Signal Debug Variable Panel to reset highlight + self.ParentWindow.ResetHighlight() + + message = None + + # Check that data is valid regarding DebugVariablePanel + try: + values = eval(data) + if not isinstance(values, TupleType): + raise ValueError + except: + message = _("Invalid value \"%s\" for debug variable")%data + values = None + + # Display message if data is invalid + if message is not None: + wx.CallAfter(self.ShowMessage, message) + + # Data contain a reference to a variable to debug + elif values[1] == "debug": + target_idx = self.ParentControl.GetIndex() + + # If mouse is dropped in graph canvas bounding box and graph is + # not 3D canvas, graphs will be merged + rect = self.ParentControl.GetAxesBoundingBox() + if not self.ParentControl.Is3DCanvas() and rect.InsideXY(x, y): + # Default merge type is parallel + merge_type = GRAPH_PARALLEL + + # If mouse is dropped in left part of graph canvas, graph + # wall be merged orthogonally + merge_rect = wx.Rect(rect.x, rect.y, + rect.width / 2., rect.height) + if merge_rect.InsideXY(x, y): + merge_type = GRAPH_ORTHOGONAL + + # Merge graphs + wx.CallAfter(self.ParentWindow.MergeGraphs, + values[0], target_idx, + merge_type, force=True) + + else: + width, height = self.ParentControl.GetSize() + + # Get Before which Viewer the variable has to be moved or added + # according to the position of mouse in Viewer. + if y > height / 2: + target_idx += 1 + + # Drag'n Drop is an internal is an internal move inside Debug + # Variable Panel + if len(values) > 2 and values[2] == "move": + self.ParentWindow.MoveValue(values[0], + target_idx) + + # Drag'n Drop was initiated by another control of Beremiz + else: + self.ParentWindow.InsertValue(values[0], + target_idx, + force=True) + + def OnLeave(self): + """ + Function called when mouse is leave Drop Target + """ + # Signal Debug Variable Panel to reset highlight + self.ParentWindow.ResetHighlight() + return wx.TextDropTarget.OnLeave(self) + + def ShowMessage(self, message): + """ + Show error message in Error Dialog + @param message: Error message to display + """ + dialog = wx.MessageDialog(self.ParentWindow, + message, + _("Error"), + wx.OK|wx.ICON_ERROR) + dialog.ShowModal() + dialog.Destroy() + + +#------------------------------------------------------------------------------- +# Debug Variable Graphic Viewer Class +#------------------------------------------------------------------------------- + +""" +Class that implements a Viewer that display variable values as a graphs +""" + +class DebugVariableGraphicViewer(DebugVariableViewer, FigureCanvas): + + def __init__(self, parent, window, items, graph_type): + """ + Constructor + @param parent: Parent wx.Window of DebugVariableText + @param window: Reference to the Debug Variable Panel + @param items: List of DebugVariableItem displayed by Viewer + @param graph_type: Graph display type (Parallel or orthogonal) + """ + DebugVariableViewer.__init__(self, window, items) + + self.GraphType = graph_type # Graph type display + self.CursorTick = None # Tick of the graph cursor + + # Mouse position when start dragging + self.MouseStartPos = None + # Tick when moving tick start + self.StartCursorTick = None + # Canvas size when starting to resize canvas + self.CanvasStartSize = None + + # List of current displayed contextual buttons + self.ContextualButtons = [] + # Reference to item for which contextual buttons was displayed + self.ContextualButtonsItem = None + + # Flag indicating that zoom fit current displayed data range or whole + # data range if False + self.ZoomFit = False + + # Create figure for drawing graphs + self.Figure = matplotlib.figure.Figure(facecolor='w') + # Defined border around figure in canvas + self.Figure.subplotpars.update(top=0.95, left=0.1, + bottom=0.1, right=0.95) + + FigureCanvas.__init__(self, parent, -1, self.Figure) + self.SetWindowStyle(wx.WANTS_CHARS) + self.SetBackgroundColour(wx.WHITE) + + # Bind wx events + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_SIZE, self.OnResize) + + # Set canvas min size + canvas_size = self.GetCanvasMinSize() + self.SetMinSize(canvas_size) + + # Define Viewer drop target + self.SetDropTarget(DebugVariableGraphicDropTarget(self, window)) + + # Connect matplotlib events + self.mpl_connect('button_press_event', self.OnCanvasButtonPressed) + self.mpl_connect('motion_notify_event', self.OnCanvasMotion) + self.mpl_connect('button_release_event', self.OnCanvasButtonReleased) + self.mpl_connect('scroll_event', self.OnCanvasScroll) + + # Add buttons for zooming on current displayed data range + self.Buttons.append( + GraphButton(0, 0, "fit_graph", self.OnZoomFitButton)) + + # Add buttons for changing canvas size with predefined height + for size, bitmap in zip( + [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI], + ["minimize_graph", "middle_graph", "maximize_graph"]): + self.Buttons.append( + GraphButton(0, 0, bitmap, + self.GetOnChangeSizeButton(size))) + + # Add buttons for exporting graph values to clipboard and close graph + for bitmap, callback in [ + ("export_graph_mini", self.OnExportGraphButton), + ("delete_graph", self.OnCloseButton)]: + self.Buttons.append(GraphButton(0, 0, bitmap, callback)) + + # Update graphs elements + self.ResetGraphics() + self.RefreshLabelsPosition(canvas_size.height) + + def AddItem(self, item): + """ + Add an item to the list of items displayed by Viewer + @param item: Item to add to the list + """ + DebugVariableViewer.AddItem(self, item) + self.ResetGraphics() + + def RemoveItem(self, item): + """ + Remove an item from the list of items displayed by Viewer + @param item: Item to remove from the list + """ + DebugVariableViewer.RemoveItem(self, item) + + # If list of items is not empty + if not self.ItemsIsEmpty(): + # Return to parallel graph if there is only one item + # especially if it's actually orthogonal + if len(self.Items) == 1: + self.GraphType = GRAPH_PARALLEL + self.ResetGraphics() + + def SetCursorTick(self, cursor_tick): + """ + Set cursor tick + @param cursor_tick: Cursor tick + """ + self.CursorTick = cursor_tick + + def SetZoomFit(self, zoom_fit): + """ + Set flag indicating that zoom fit current displayed data range + @param zoom_fit: Flag for zoom fit (False: zoom fit whole data range) + """ + # Flag is different from the actual one + if zoom_fit != self.ZoomFit: + # Save new flag value + self.ZoomFit = zoom_fit + + # Update button for zoom fit bitmap + self.Buttons[0].SetBitmap("full_graph" if zoom_fit else "fit_graph") + + # Refresh canvas + self.RefreshViewer() + + def SubscribeAllDataConsumers(self): + """ + Function that unsubscribe and remove every item that store values of + a variable that doesn't exist in PLC anymore + """ + DebugVariableViewer.SubscribeAllDataConsumers(self) + + # Graph still have data to display + if not self.ItemsIsEmpty(): + # Reset flag indicating that zoom fit current displayed data range + self.SetZoomFit(False) + + self.ResetGraphics() + + def Is3DCanvas(self): + """ + Return if Viewer is a 3D canvas + @return: True if Viewer is a 3D canvas + """ + return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3 + + def GetButtons(self): + """ + Return list of buttons defined in Viewer + @return: List of buttons + """ + # Add contextual buttons to default buttons + return self.Buttons + self.ContextualButtons + + def PopupContextualButtons(self, item, rect, direction=wx.RIGHT): + """ + Show contextual menu for item aside a label of this item defined + by the bounding box of label in figure + @param item: Item for which contextual is shown + @param rect: Bounding box of label aside which drawing menu + @param direction: Direction in which buttons must be drawn + """ + # Return immediately if contextual menu for item is already shown + if self.ContextualButtonsItem == item: + return + + # Close already shown contextual menu + self.DismissContextualButtons() + + # Save item for which contextual menu is shown + self.ContextualButtonsItem = item + + # If item variable is forced, add button for release variable to + # contextual menu + if self.ContextualButtonsItem.IsForced(): + self.ContextualButtons.append( + GraphButton(0, 0, "release", self.OnReleaseItemButton)) + + # Add other buttons to contextual menu + for bitmap, callback in [ + ("force", self.OnForceItemButton), + ("export_graph_mini", self.OnExportItemGraphButton), + ("delete_graph", self.OnRemoveItemButton)]: + self.ContextualButtons.append( + GraphButton(0, 0, bitmap, callback)) + + # If buttons are shown at left side or upper side of rect, positions + # will be set in reverse order + buttons = self.ContextualButtons[:] + if direction in [wx.TOP, wx.LEFT]: + buttons.reverse() + + # Set contextual menu buttons position aside rect depending on + # direction given + offset = 0 + for button in buttons: + w, h = button.GetSize() + if direction in [wx.LEFT, wx.RIGHT]: + x = rect.x + (- w - offset + if direction == wx.LEFT + else rect.width + offset) + y = rect.y + (rect.height - h) / 2 + offset += w + else: + x = rect.x + (rect.width - w ) / 2 + y = rect.y + (- h - offset + if direction == wx.TOP + else rect.height + offset) + offset += h + button.SetPosition(x, y) + button.Show() + + # Refresh canvas + self.ParentWindow.ForceRefresh() + + def DismissContextualButtons(self): + """ + Close current shown contextual menu + """ + # Return immediately if no contextual menu is shown + if self.ContextualButtonsItem is None: + return + + # Reset variables corresponding to contextual menu + self.ContextualButtonsItem = None + self.ContextualButtons = [] + + # Refresh canvas + self.ParentWindow.ForceRefresh() + + def IsOverContextualButton(self, x, y): + """ + Return if point is over one contextual button of Viewer + @param x: X coordinate of point + @param y: Y coordinate of point + @return: contextual button where point is over + """ + for button in self.ContextualButtons: + if button.HitTest(x, y): + return button + return None + + def ExportGraph(self, item=None): + """ + Export item(s) data to clipboard in CSV format + @param item: Item from which data to export, all items if None + (default None) + """ + self.ParentWindow.CopyDataToClipboard( + [(item, [entry for entry in item.GetData()]) + for item in (self.Items + if item is None + else [item])]) + + def OnZoomFitButton(self): + """ + Function called when Viewer Zoom Fit button is pressed + """ + # Toggle zoom fit flag value + self.SetZoomFit(not self.ZoomFit) + + def GetOnChangeSizeButton(self, height): + """ + Function that generate callback function for change Viewer height to + pre-defined height button + @param height: Height that change Viewer to + @return: callback function + """ + def OnChangeSizeButton(): + self.SetCanvasHeight(height) + return OnChangeSizeButton + + def OnExportGraphButton(self): + """ + Function called when Viewer Export button is pressed + """ + # Export data of every item in Viewer + self.ExportGraph() + + def OnForceItemButton(self): + """ + Function called when contextual menu Force button is pressed + """ + # Open dialog for forcing item variable value + self.ForceValue(self.ContextualButtonsItem) + # Close contextual menu + self.DismissContextualButtons() + + def OnReleaseItemButton(self): + """ + Function called when contextual menu Release button is pressed + """ + # Release item variable value + self.ReleaseValue(self.ContextualButtonsItem) + # Close contextual menu + self.DismissContextualButtons() + + def OnExportItemGraphButton(self): + """ + Function called when contextual menu Export button is pressed + """ + # Export data of item variable + self.ExportGraph(self.ContextualButtonsItem) + # Close contextual menu + self.DismissContextualButtons() + + def OnRemoveItemButton(self): + """ + Function called when contextual menu Remove button is pressed + """ + # Remove item from Viewer + wx.CallAfter(self.ParentWindow.DeleteValue, self, + self.ContextualButtonsItem) + # Close contextual menu + self.DismissContextualButtons() + + def HandleCursorMove(self, event): + """ + Update Cursor position according to mouse position and graph type + @param event: Mouse event + """ + start_tick, end_tick = self.ParentWindow.GetRange() + cursor_tick = None + items = self.ItemsDict.values() + + # Graph is orthogonal + if self.GraphType == GRAPH_ORTHOGONAL: + # Extract items data displayed in canvas figure + start_tick = max(start_tick, self.GetItemsMinCommonTick()) + end_tick = max(end_tick, start_tick) + x_data = items[0].GetData(start_tick, end_tick) + y_data = items[1].GetData(start_tick, end_tick) + + # Search for the nearest point from mouse position + if len(x_data) > 0 and len(y_data) > 0: + length = min(len(x_data), len(y_data)) + d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + \ + (y_data[:length,1]-event.ydata) ** 2) + + # Set cursor tick to the tick of this point + cursor_tick = x_data[numpy.argmin(d), 0] + + # Graph is parallel + else: + # Extract items tick + data = items[0].GetData(start_tick, end_tick) + + # Search for point that tick is the nearest from mouse X position + # and set cursor tick to the tick of this point + if len(data) > 0: + cursor_tick = data[numpy.argmin( + numpy.abs(data[:,0] - event.xdata)), 0] + + # Update cursor tick + if cursor_tick is not None: + self.ParentWindow.SetCursorTick(cursor_tick) + + def OnCanvasButtonPressed(self, event): + """ + Function called when a button of mouse is pressed + @param event: Mouse event + """ + # Get mouse position, graph Y coordinate is inverted in matplotlib + # comparing to wx + width, height = self.GetSize() + x, y = event.x, height - event.y + + # Return immediately if mouse is over a button + if self.IsOverButton(x, y): + return + + # Mouse was clicked inside graph figure + if event.inaxes == self.Axes: + + # Find if it was on an item label + item_idx = None + # Check every label paired with corresponding item + for i, t in ([pair for pair in enumerate(self.AxesLabels)] + + [pair for pair in enumerate(self.Labels)]): + # Get label bounding box + (x0, y0), (x1, y1) = t.get_window_extent().get_points() + rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) + # Check if mouse was over label + if rect.InsideXY(x, y): + item_idx = i + break + + # If an item label have been clicked + if item_idx is not None: + # Hide buttons and contextual buttons + self.ShowButtons(False) + self.DismissContextualButtons() + + # Start a drag'n drop from mouse position in wx coordinate of + # parent + xw, yw = self.GetPosition() + self.ParentWindow.StartDragNDrop(self, + self.ItemsDict.values()[item_idx], + x + xw, y + yw, # Current mouse position + x + xw, y + yw) # Mouse position when button was clicked + + # Don't handle mouse button if canvas is 3D and let matplotlib do + # the default behavior (rotate 3D axes) + elif not self.Is3DCanvas(): + # Save mouse position when clicked + self.MouseStartPos = wx.Point(x, y) + + # Mouse button was left button, start moving cursor + if event.button == 1: + # Save current tick in case a drag'n drop is initiate to + # restore it + self.StartCursorTick = self.CursorTick + + self.HandleCursorMove(event) + + # Mouse button is middle button and graph is parallel, start + # moving graph along X coordinate (tick) + elif event.button == 2 and self.GraphType == GRAPH_PARALLEL: + self.StartCursorTick = self.ParentWindow.GetRange()[0] + + # Mouse was clicked outside graph figure and over resize highlight with + # left button, start resizing Viewer + elif event.button == 1 and event.y <= 5: + self.MouseStartPos = wx.Point(x, y) + self.CanvasStartSize = height + + def OnCanvasButtonReleased(self, event): + """ + Function called when a button of mouse is released + @param event: Mouse event + """ + # If a drag'n drop is in progress, stop it + if self.ParentWindow.IsDragging(): + width, height = self.GetSize() + xw, yw = self.GetPosition() + item = self.ParentWindow.DraggingAxesPanel.ItemsDict.values()[0] + # Give mouse position in wx coordinate of parent + self.ParentWindow.StopDragNDrop(item.GetVariable(), + xw + event.x, yw + height - event.y) + + else: + # Reset any move in progress + self.MouseStartPos = None + self.CanvasStartSize = None + + # Handle button under mouse if it exist + width, height = self.GetSize() + self.HandleButton(event.x, height - event.y) + + def OnCanvasMotion(self, event): + """ + Function called when a button of mouse is moved over Viewer + @param event: Mouse event + """ + width, height = self.GetSize() + + # If a drag'n drop is in progress, move canvas dragged + if self.ParentWindow.IsDragging(): + xw, yw = self.GetPosition() + # Give mouse position in wx coordinate of parent + self.ParentWindow.MoveDragNDrop( + xw + event.x, + yw + height - event.y) + + # If a Viewer resize is in progress, change Viewer size + elif event.button == 1 and self.CanvasStartSize is not None: + width, height = self.GetSize() + self.SetCanvasHeight( + self.CanvasStartSize + height - event.y - self.MouseStartPos.y) + + # If no button is pressed, show or hide contextual buttons or resize + # highlight + elif event.button is None: + # Compute direction for items label according graph type + if self.GraphType == GRAPH_PARALLEL: # Graph is parallel + directions = [wx.RIGHT] * len(self.AxesLabels) + \ + [wx.LEFT] * len(self.Labels) + elif len(self.AxesLabels) > 0: # Graph is orthogonal in 2D + directions = [wx.RIGHT, wx.TOP, # Directions for AxesLabels + wx.LEFT, wx.BOTTOM] # Directions for Labels + else: # Graph is orthogonal in 3D + directions = [wx.LEFT] * len(self.Labels) + + # Find if mouse is over an item label + item_idx = None + menu_direction = None + for (i, t), dir in zip( + [pair for pair in enumerate(self.AxesLabels)] + + [pair for pair in enumerate(self.Labels)], + directions): + # Check every label paired with corresponding item + (x0, y0), (x1, y1) = t.get_window_extent().get_points() + rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) + # Check if mouse was over label + if rect.InsideXY(event.x, height - event.y): + item_idx = i + menu_direction = dir + break + + # If mouse is over an item label, + if item_idx is not None: + self.PopupContextualButtons( + self.ItemsDict.values()[item_idx], + rect, menu_direction) + return + + # If mouse isn't over a contextual menu, hide the current shown one + # if it exists + if self.IsOverContextualButton(event.x, height - event.y) is None: + self.DismissContextualButtons() + + # Update resize highlight + if event.y <= 5: + if self.SetHighlight(HIGHLIGHT_RESIZE): + self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) + self.ParentWindow.ForceRefresh() + else: + if self.SetHighlight(HIGHLIGHT_NONE): + self.SetCursor(wx.NullCursor) + self.ParentWindow.ForceRefresh() + + # Handle buttons if canvas is not 3D + elif not self.Is3DCanvas(): + + # If left button is pressed + if event.button == 1: + + # Mouse is inside graph figure + if event.inaxes == self.Axes: + + # If a cursor move is in progress, update cursor position + if self.MouseStartPos is not None: + self.HandleCursorMove(event) + + # Mouse is outside graph figure, cursor move is in progress and + # there is only one item in Viewer, start a drag'n drop + elif self.MouseStartPos is not None and len(self.Items) == 1: + xw, yw = self.GetPosition() + self.ParentWindow.SetCursorTick(self.StartCursorTick) + self.ParentWindow.StartDragNDrop(self, + self.ItemsDict.values()[0], + # Current mouse position + event.x + xw, height - event.y + yw, + # Mouse position when button was clicked + self.MouseStartPos.x + xw, + self.MouseStartPos.y + yw) + + # If middle button is pressed and moving graph along X coordinate + # is in progress + elif event.button == 2 and self.GraphType == GRAPH_PARALLEL and \ + self.MouseStartPos is not None: + start_tick, end_tick = self.ParentWindow.GetRange() + rect = self.GetAxesBoundingBox() + + # Move graph along X coordinate + self.ParentWindow.SetCanvasPosition( + self.StartCursorTick + + (self.MouseStartPos.x - event.x) * + (end_tick - start_tick) / rect.width) + + def OnCanvasScroll(self, event): + """ + Function called when a wheel mouse is use in Viewer + @param event: Mouse event + """ + # Change X range of graphs if mouse is in canvas figure and ctrl is + # pressed + if event.inaxes is not None and event.guiEvent.ControlDown(): + + # Calculate position of fixed tick point according to graph type + # and mouse position + if self.GraphType == GRAPH_ORTHOGONAL: + start_tick, end_tick = self.ParentWindow.GetRange() + tick = (start_tick + end_tick) / 2. + else: + tick = event.xdata + self.ParentWindow.ChangeRange(int(-event.step) / 3, tick) + + # Vetoing event to prevent parent panel to be scrolled + self.ParentWindow.VetoScrollEvent = True + + def OnLeftDClick(self, event): + """ + Function called when a left mouse button is double clicked + @param event: Mouse event + """ + # Check that double click was done inside figure + pos = event.GetPosition() + rect = self.GetAxesBoundingBox() + if rect.InsideXY(pos.x, pos.y): + # Reset Cursor tick to value before starting clicking + self.ParentWindow.SetCursorTick(self.StartCursorTick) + # Toggle to text Viewer(s) + self.ParentWindow.ToggleViewerType(self) + + else: + event.Skip() + + # Cursor tick move for each arrow key + KEY_CURSOR_INCREMENT = { + wx.WXK_LEFT: -1, + wx.WXK_RIGHT: 1, + wx.WXK_UP: 10, + wx.WXK_DOWN: -10} + + def OnKeyDown(self, event): + """ + Function called when key is pressed + @param event: wx.KeyEvent + """ + # If cursor is shown and arrow key is pressed, move cursor tick + if self.CursorTick is not None: + move = self.KEY_CURSOR_INCREMENT.get(event.GetKeyCode(), None) + if move is not None: + self.ParentWindow.MoveCursorTick(move) + event.Skip() + + def OnLeave(self, event): + """ + Function called when mouse leave Viewer + @param event: wx.MouseEvent + """ + # If Viewer is not resizing, reset resize highlight + if self.CanvasStartSize is None: + self.SetHighlight(HIGHLIGHT_NONE) + self.SetCursor(wx.NullCursor) + DebugVariableViewer.OnLeave(self, event) + else: + event.Skip() + + def GetCanvasMinSize(self): + """ + Return the minimum size of Viewer so that all items label can be + displayed + @return: wx.Size containing Viewer minimum size + """ + # The minimum height take in account the height of all items, padding + # inside figure and border around figure + return wx.Size(200, + CANVAS_BORDER[0] + CANVAS_BORDER[1] + + 2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items)) + + def SetCanvasHeight(self, height): + """ + Set Viewer size checking that it respects Viewer minimum size + @param height: Viewer height + """ + min_width, min_height = self.GetCanvasMinSize() + height = max(height, min_height) + self.SetMinSize(wx.Size(min_width, height)) + self.RefreshLabelsPosition(height) + self.ParentWindow.RefreshGraphicsSizer() + + def GetAxesBoundingBox(self, parent_coordinate=False): + """ + Return figure bounding box in wx coordinate + @param parent_coordinate: True if use parent coordinate (default False) + """ + # Calculate figure bounding box. Y coordinate is inverted in matplotlib + # figure comparing to wx panel + width, height = self.GetSize() + ax, ay, aw, ah = self.figure.gca().get_position().bounds + bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1, + aw * width + 2, ah * height + 1) + + # If parent_coordinate, add Viewer position in parent + if parent_coordinate: + xw, yw = self.GetPosition() + bbox.x += xw + bbox.y += yw + + return bbox + + def RefreshHighlight(self, x, y): + """ + Refresh Viewer highlight according to mouse position + @param x: X coordinate of mouse pointer + @param y: Y coordinate of mouse pointer + """ + width, height = self.GetSize() + + # Mouse is over Viewer figure and graph is not 3D + bbox = self.GetAxesBoundingBox() + if bbox.InsideXY(x, y) and not self.Is3DCanvas(): + rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height) + # Mouse is over Viewer left part of figure + if rect.InsideXY(x, y): + self.SetHighlight(HIGHLIGHT_LEFT) + + # Mouse is over Viewer right part of figure + else: + self.SetHighlight(HIGHLIGHT_RIGHT) + + # Mouse is over upper part of Viewer + elif y < height / 2: + # Viewer is upper one in Debug Variable Panel, show highlight + if self.ParentWindow.IsViewerFirst(self): + self.SetHighlight(HIGHLIGHT_BEFORE) + + # Viewer is not the upper one, show highlight in previous one + # It prevents highlight to move when mouse leave one Viewer to + # another + else: + self.SetHighlight(HIGHLIGHT_NONE) + self.ParentWindow.HighlightPreviousViewer(self) + + # Mouse is over lower part of Viewer + else: + self.SetHighlight(HIGHLIGHT_AFTER) + + def OnAxesMotion(self, event): + """ + Function overriding default function called when mouse is dragged for + rotating graph preventing refresh to be called too quickly + @param event: Mouse event + """ + if self.Is3DCanvas(): + # Call default function at most 10 times per second + current_time = gettime() + if current_time - self.LastMotionTime > REFRESH_PERIOD: + self.LastMotionTime = current_time + Axes3D._on_move(self.Axes, event) + + def GetAddTextFunction(self): + """ + Return function for adding text in figure according to graph type + @return: Function adding text to figure + """ + text_func = (self.Axes.text2D if self.Is3DCanvas() else self.Axes.text) + def AddText(*args, **kwargs): + args = [0, 0, ""] + kwargs["transform"] = self.Axes.transAxes + return text_func(*args, **kwargs) + return AddText + + def ResetGraphics(self): + """ + Reset figure and graphical elements displayed in it + Called any time list of items or graph type change + """ + # Clear figure from any axes defined + self.Figure.clear() + + # Add 3D projection if graph is in 3D + if self.Is3DCanvas(): + self.Axes = self.Figure.gca(projection='3d') + self.Axes.set_color_cycle(['b']) + + # Override function to prevent too much refresh when graph is + # rotated + self.LastMotionTime = gettime() + setattr(self.Axes, "_on_move", self.OnAxesMotion) + + # Init graph mouse event so that graph can be rotated + self.Axes.mouse_init() + + # Set size of Z axis labels + self.Axes.tick_params(axis='z', labelsize='small') + + else: + self.Axes = self.Figure.gca() + self.Axes.set_color_cycle(COLOR_CYCLE) + + # Set size of X and Y axis labels + self.Axes.tick_params(axis='x', labelsize='small') + self.Axes.tick_params(axis='y', labelsize='small') + + # Init variables storing graphical elements added to figure + self.Plots = [] # List of curves + self.VLine = None # Vertical line for cursor + self.HLine = None # Horizontal line for cursor (only orthogonal 2D) + self.AxesLabels = [] # List of items variable path text label + self.Labels = [] # List of items text label + + # Get function to add a text in figure according to graph type + add_text_func = self.GetAddTextFunction() + + # Graph type is parallel or orthogonal in 3D + if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): + num_item = len(self.Items) + for idx in xrange(num_item): + + # Get color from color cycle (black if only one item) + color = ('k' if num_item == 1 + else COLOR_CYCLE[idx % len(COLOR_CYCLE)]) + + # In 3D graph items variable label are not displayed as text + # in figure, but as axis title + if not self.Is3DCanvas(): + # Items variable labels are in figure upper left corner + self.AxesLabels.append( + add_text_func(size='small', color=color, + verticalalignment='top')) + + # Items variable labels are in figure lower right corner + self.Labels.append( + add_text_func(size='large', color=color, + horizontalalignment='right')) + + # Graph type is orthogonal in 2D + else: + # X coordinate labels are in figure lower side + self.AxesLabels.append(add_text_func(size='small')) + self.Labels.append( + add_text_func(size='large', + horizontalalignment='right')) + + # Y coordinate labels are vertical and in figure left side + self.AxesLabels.append( + add_text_func(size='small', rotation='vertical', + verticalalignment='bottom')) + self.Labels.append( + add_text_func(size='large', rotation='vertical', + verticalalignment='top')) + + # Refresh position of labels according to Viewer size + width, height = self.GetSize() + self.RefreshLabelsPosition(height) + + def RefreshLabelsPosition(self, height): + """ + Function called when mouse leave Viewer + @param event: wx.MouseEvent + """ + # Figure position like text position in figure are expressed is ratio + # canvas size and figure size. As we want that border around figure and + # text position in figure don't change when canvas size change, we + # expressed border and text position in pixel on screen and apply the + # ratio calculated hereafter to get border and text position in + # matplotlib coordinate + canvas_ratio = 1. / height # Divide by canvas height in pixel + graph_ratio = 1. / ( + (1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) + * height) # Divide by figure height in pixel + + # Update position of figure (keeping up and bottom border the same + # size) + self.Figure.subplotpars.update( + top= 1.0 - CANVAS_BORDER[1] * canvas_ratio, + bottom= CANVAS_BORDER[0] * canvas_ratio) + + # Update position of items labels + if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): + num_item = len(self.Items) + for idx in xrange(num_item): + + # In 3D graph items variable label are not displayed + if not self.Is3DCanvas(): + # Items variable labels are in figure upper left corner + self.AxesLabels[idx].set_position( + (0.05, + 1.0 - (CANVAS_PADDING + + AXES_LABEL_HEIGHT * idx) * graph_ratio)) + + # Items variable labels are in figure lower right corner + self.Labels[idx].set_position( + (0.95, + CANVAS_PADDING * graph_ratio + + (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio)) + else: + # X coordinate labels are in figure lower side + self.AxesLabels[0].set_position( + (0.1, CANVAS_PADDING * graph_ratio)) + self.Labels[0].set_position( + (0.95, CANVAS_PADDING * graph_ratio)) + + # Y coordinate labels are vertical and in figure left side + self.AxesLabels[1].set_position( + (0.05, 2 * CANVAS_PADDING * graph_ratio)) + self.Labels[1].set_position( + (0.05, 1.0 - CANVAS_PADDING * graph_ratio)) + + # Update subplots + self.Figure.subplots_adjust() + + def RefreshViewer(self, refresh_graphics=True): + """ + Function called to refresh displayed by matplotlib canvas + @param refresh_graphics: Flag indicating that graphs have to be + refreshed (False: only label values have to be refreshed) + """ + # Refresh graphs if needed + if refresh_graphics: + # Get tick range of values to display + start_tick, end_tick = self.ParentWindow.GetRange() + + # Graph is parallel + if self.GraphType == GRAPH_PARALLEL: + # Init list of data range for each variable displayed + ranges = [] + + # Get data and range for each variable displayed + for idx, item in enumerate(self.Items): + data, min_value, max_value = item.GetDataAndValueRange( + start_tick, end_tick, not self.ZoomFit) + + # Check that data is not empty + if data is not None: + # Add variable range to list of variable data range + ranges.append((min_value, max_value)) + + # Add plot to canvas if not yet created + if len(self.Plots) <= idx: + self.Plots.append( + self.Axes.plot(data[:, 0], data[:, 1])[0]) + + # Set data to already created plot in canvas + else: + self.Plots[idx].set_data(data[:, 0], data[:, 1]) + + # Get X and Y axis ranges + x_min, x_max = start_tick, end_tick + y_min, y_max = merge_ranges(ranges) + + # Display cursor in canvas if a cursor tick is defined and it is + # include in values tick range + if (self.CursorTick is not None and + start_tick <= self.CursorTick <= end_tick): + + # Define a vertical line to display cursor position if no + # line is already defined + if self.VLine is None: + self.VLine = self.Axes.axvline(self.CursorTick, + color=CURSOR_COLOR) + + # Set value of vertical line if already defined + else: + self.VLine.set_xdata((self.CursorTick, self.CursorTick)) + self.VLine.set_visible(True) + + # Hide vertical line if cursor tick is not defined or reset + elif self.VLine is not None: + self.VLine.set_visible(False) + + # Graph is orthogonal + else: + # Update tick range, removing ticks that don't have a value for + # each variable + start_tick = max(start_tick, self.GetItemsMinCommonTick()) + end_tick = max(end_tick, start_tick) + items = self.ItemsDict.values() + + # Get data and range for first variable (X coordinate) + x_data, x_min, x_max = items[0].GetDataAndValueRange( + start_tick, end_tick, not self.ZoomFit) + # Get data and range for second variable (Y coordinate) + y_data, y_min, y_max = items[1].GetDataAndValueRange( + start_tick, end_tick, not self.ZoomFit) + + # Normalize X and Y coordinates value range + x_min, x_max = merge_ranges([(x_min, x_max)]) + y_min, y_max = merge_ranges([(y_min, y_max)]) + + # Get X and Y coordinates for cursor if cursor tick is defined + if self.CursorTick is not None: + x_cursor, x_forced = items[0].GetValue( + self.CursorTick, raw=True) + y_cursor, y_forced = items[1].GetValue( + self.CursorTick, raw=True) + + # Get common data length so that each value has an x and y + # coordinate + length = (min(len(x_data), len(y_data)) + if x_data is not None and y_data is not None + else 0) + + # Graph is orthogonal 2D + if len(self.Items) < 3: + + # Check that x and y data are not empty + if x_data is not None and y_data is not None: + + # Add plot to canvas if not yet created + if len(self.Plots) == 0: + self.Plots.append( + self.Axes.plot(x_data[:, 1][:length], + y_data[:, 1][:length])[0]) + + # Set data to already created plot in canvas + else: + self.Plots[0].set_data( + x_data[:, 1][:length], + y_data[:, 1][:length]) + + # Display cursor in canvas if a cursor tick is defined and it is + # include in values tick range + if (self.CursorTick is not None and + start_tick <= self.CursorTick <= end_tick): + + # Define a vertical line to display cursor x coordinate + # if no line is already defined + if self.VLine is None: + self.VLine = self.Axes.axvline(x_cursor, + color=CURSOR_COLOR) + # Set value of vertical line if already defined + else: + self.VLine.set_xdata((x_cursor, x_cursor)) + + + # Define a horizontal line to display cursor y + # coordinate if no line is already defined + if self.HLine is None: + self.HLine = self.Axes.axhline(y_cursor, + color=CURSOR_COLOR) + # Set value of horizontal line if already defined + else: + self.HLine.set_ydata((y_cursor, y_cursor)) + + self.VLine.set_visible(True) + self.HLine.set_visible(True) + + # Hide vertical and horizontal line if cursor tick is not + # defined or reset + else: + if self.VLine is not None: + self.VLine.set_visible(False) + if self.HLine is not None: + self.HLine.set_visible(False) + + # Graph is orthogonal 3D + else: + # Remove all plots already defined in 3D canvas + while len(self.Axes.lines) > 0: + self.Axes.lines.pop() + + # Get data and range for third variable (Z coordinate) + z_data, z_min, z_max = items[2].GetDataAndValueRange( + start_tick, end_tick, not self.ZoomFit) + + # Normalize Z coordinate value range + z_min, z_max = merge_ranges([(z_min, z_max)]) + + # Check that x, y and z data are not empty + if (x_data is not None and y_data is not None and + z_data is not None): + + # Get common data length so that each value has an x, y + # and z coordinate + length = min(length, len(z_data)) + + # Add plot to canvas + self.Axes.plot(x_data[:, 1][:length], + y_data[:, 1][:length], + zs = z_data[:, 1][:length]) + + # Display cursor in canvas if a cursor tick is defined and + # it is include in values tick range + if (self.CursorTick is not None and + start_tick <= self.CursorTick <= end_tick): + + # Get Z coordinate for cursor + z_cursor, z_forced = items[2].GetValue( + self.CursorTick, raw=True) + + # Add 3 lines parallel to x, y and z axis to display + # cursor position in 3D + for kwargs in [{"xs": numpy.array([x_min, x_max])}, + {"ys": numpy.array([y_min, y_max])}, + {"zs": numpy.array([z_min, z_max])}]: + for param, value in [ + ("xs", numpy.array([x_cursor, x_cursor])), + ("ys", numpy.array([y_cursor, y_cursor])), + ("zs", numpy.array([z_cursor, z_cursor]))]: + kwargs.setdefault(param, value) + kwargs["color"] = CURSOR_COLOR + self.Axes.plot(**kwargs) + + # Set Z axis limits + self.Axes.set_zlim(z_min, z_max) + + # Set X and Y axis limits + self.Axes.set_xlim(x_min, x_max) + self.Axes.set_ylim(y_min, y_max) + + # Get value and forced flag for each variable displayed in graph + # If cursor tick is not defined get value and flag of last received + # or get value and flag of variable at cursor tick + values, forced = apply(zip, [ + (item.GetValue(self.CursorTick) + if self.CursorTick is not None + else (item.GetValue(), item.IsForced())) + for item in self.Items]) + + # Get path of each variable displayed simplified using panel variable + # name mask + labels = [item.GetVariable(self.ParentWindow.GetVariableNameMask()) + for item in self.Items] + + # Get style for each variable according to + styles = map(lambda x: {True: 'italic', False: 'normal'}[x], forced) + + # Graph is orthogonal 3D, set variables path as 3D axis label + if self.Is3DCanvas(): + for idx, label_func in enumerate([self.Axes.set_xlabel, + self.Axes.set_ylabel, + self.Axes.set_zlabel]): + label_func(labels[idx], fontdict={'size': 'small', + 'color': COLOR_CYCLE[idx]}) + + # Graph is not orthogonal 3D, set variables path in axes labels + else: + for label, text in zip(self.AxesLabels, labels): + label.set_text(text) + + # Set value label text and style according to value and forced flag for + # each variable displayed + for label, value, style in zip(self.Labels, values, styles): + label.set_text(value) + label.set_style(style) + + # Refresh figure + self.draw() + + def draw(self, drawDC=None): + """ + Render the figure. + """ + # Render figure using agg + FigureCanvasAgg.draw(self) + + # Get bitmap of figure rendered + self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) + self.bitmap.UseAlpha() + + # Create DC for rendering graphics in bitmap + destDC = wx.MemoryDC() + destDC.SelectObject(self.bitmap) + + # Get Graphics Context for DC, for anti-aliased and transparent + # rendering + destGC = wx.GCDC(destDC) + + destGC.BeginDrawing() + + # Get canvas size and figure bounding box in canvas + width, height = self.GetSize() + bbox = self.GetAxesBoundingBox() + + # If highlight to display is resize, draw thick grey line at bottom + # side of canvas + if self.Highlight == HIGHLIGHT_RESIZE: + destGC.SetPen(HIGHLIGHT_RESIZE_PEN) + destGC.SetBrush(HIGHLIGHT_RESIZE_BRUSH) + destGC.DrawRectangle(0, height - 5, width, 5) + + # If highlight to display is merging graph, draw 50% transparent blue + # rectangle on left or right part of figure depending on highlight type + elif self.Highlight in [HIGHLIGHT_LEFT, HIGHLIGHT_RIGHT]: + destGC.SetPen(HIGHLIGHT_DROP_PEN) + destGC.SetBrush(HIGHLIGHT_DROP_BRUSH) + + x_offset = (bbox.width / 2 + if self.Highlight == HIGHLIGHT_RIGHT + else 0) + destGC.DrawRectangle(bbox.x + x_offset, bbox.y, + bbox.width / 2, bbox.height) + + # Draw other Viewer common elements + self.DrawCommonElements(destGC, self.GetButtons()) + + destGC.EndDrawing() + + self._isDrawn = True + self.gui_repaint(drawDC=drawDC) + +