# HG changeset patch # User Laurent Bessard # Date 1360078972 -3600 # Node ID 5f2cc382be8c692bd9f23387c51bcb368c99da0e # Parent 1499a4d225dbac3fe18ac907062865fd14ec2717 Added support for displaying string variables variations in a graph and cursor on graphs diff -r 1499a4d225db -r 5f2cc382be8c controls/DebugVariablePanel.py --- a/controls/DebugVariablePanel.py Wed Jan 30 23:46:41 2013 +0100 +++ b/controls/DebugVariablePanel.py Tue Feb 05 16:42:52 2013 +0100 @@ -26,6 +26,7 @@ from time import time as gettime import math import numpy +import binascii import wx import wx.lib.buttons @@ -51,7 +52,10 @@ def GetDebugVariablesTableColnames(): _ = lambda x : x return [_("Variable"), _("Value")] - + +CRC_SIZE = 8 +CRC_MASK = 2 ** CRC_SIZE - 1 + class VariableTableItem(DebugDataConsumer): def __init__(self, parent, variable): @@ -70,12 +74,21 @@ self.RefreshVariableType() self.Parent.RefreshView() - def GetVariable(self, max_size=None): + def GetVariable(self, mask=None): variable = self.Variable - if max_size is not None: - max_size = max(max_size, 10) - if len(variable) > max_size: - variable = "..." + variable[-(max_size - 3):] + if mask is not None: + parts = variable.split('.') + mask = mask + ['*'] * max(0, len(parts) - len(mask)) + last = None + variable = "" + for m, v in zip(mask, parts): + if m == '*': + if last == '*': + variable += '.' + variable += v + elif last is None or last == '*': + variable += '..' + last = m return variable def RefreshVariableType(self): @@ -105,23 +118,34 @@ return None + def GetRawValue(self, idx): + if self.VariableType in ["STRING", "WSTRING"] and idx < len(self.RawData): + return self.RawData[idx][0] + return "" + def GetRange(self): return self.MinValue, self.MaxValue def ResetData(self): if self.IsNumVariable(): - self.Data = numpy.array([]).reshape(0, 2) + self.Data = numpy.array([]).reshape(0, 3) + if self.VariableType in ["STRING", "WSTRING"]: + self.RawData = [] self.MinValue = None self.MaxValue = None else: self.Data = None def IsNumVariable(self): - return self.Parent.IsNumType(self.VariableType) + return (self.Parent.IsNumType(self.VariableType) or + self.VariableType in ["STRING", "WSTRING"]) def NewValue(self, tick, value, forced=False): if USE_MPL and self.IsNumVariable(): - num_value = {True:1., False:0.}.get(value, float(value)) + if self.VariableType in ["STRING", "WSTRING"]: + num_value = binascii.crc32(value) & CRC_MASK + else: + num_value = float(value) if self.MinValue is None: self.MinValue = num_value else: @@ -130,7 +154,17 @@ self.MaxValue = num_value else: self.MaxValue = max(self.MaxValue, num_value) - self.Data = numpy.append(self.Data, [[float(tick), num_value]], axis=0) + forced_value = float(forced) + if self.VariableType in ["STRING", "WSTRING"]: + raw_data = (value, forced_value) + if len(self.RawData) == 0 or self.RawData[-1] != raw_data: + extra_value = len(self.RawData) + self.RawData.append(raw_data) + else: + extra_value = len(self.RawData) - 1 + else: + extra_value = forced_value + self.Data = numpy.append(self.Data, [[float(tick), num_value, extra_value]], axis=0) self.Parent.HasNewData = True DebugDataConsumer.NewValue(self, tick, value, forced) @@ -147,13 +181,29 @@ self.Value = value self.Parent.HasNewData = True - def GetValue(self): - if self.VariableType == "STRING": - return "'%s'" % self.Value - elif self.VariableType == "WSTRING": - return "\"%s\"" % self.Value - elif isinstance(self.Value, FloatType): - return "%.6g" % self.Value + def GetValue(self, tick=None, raw=False): + if tick is not None and self.IsNumVariable() and len(self.Data) > 0: + idx = numpy.argmin(abs(self.Data[:, 0] - tick)) + if self.VariableType in ["STRING", "WSTRING"]: + value, forced = self.RawData[int(self.Data[idx, 2])] + if not raw: + if self.VariableType == "STRING": + value = "'%s'" % value + else: + value = '"%s"' % value + return value, forced + else: + value = self.Data[idx, 1] + if not raw and isinstance(value, FloatType): + value = "%.6g" % value + return value, self.IsForced() + elif not raw: + if self.VariableType == "STRING": + return "'%s'" % self.Value + elif self.VariableType == "WSTRING": + return '"%s"' % self.Value + elif isinstance(self.Value, FloatType): + return "%.6g" % self.Value return self.Value def GetNearestData(self, tick, adjust): @@ -307,9 +357,11 @@ dialog.Destroy() if USE_MPL: - SECOND = 1000000000 + MILLISECOND = 1000000 + SECOND = 1000 * MILLISECOND MINUTE = 60 * SECOND HOUR = 60 * MINUTE + DAY = 24 * HOUR ZOOM_VALUES = map(lambda x:("x %.1f" % x, x), [math.sqrt(2) ** i for i in xrange(8)]) RANGE_VALUES = map(lambda x: (str(x), x), [25 * 2 ** i for i in xrange(6)]) @@ -323,7 +375,7 @@ def NextTick(variables): next_tick = None - for var_name, data in variables: + for item, data in variables: if len(data) > 0: if next_tick is None: next_tick = data[0][0] @@ -357,6 +409,7 @@ self.ParentWindow = window self.Items = items + self.ResetVariableNameMask() self.MainSizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0) self.AddViewer() @@ -388,17 +441,29 @@ return variables return self.Items[0].GetVariable() + def ResetVariableNameMask(self): + if len(self.Items) > 1: + self.VariableNameMask = reduce(compute_mask, + [item.GetVariable().split('.') for item in self.Items]) + elif len(self.Items) > 0: + self.VariableNameMask = self.Items[0].GetVariable().split('.')[:-1] + ['*'] + else: + self.VariableNameMask = [] + def AddItem(self, item): self.Items.append(item) + self.ResetVariableNameMask() def RemoveItem(self, item): if item in self.Items: self.Items.remove(item) + self.ResetVariableNameMask() def Clear(self): for item in self.Items: self.ParentWindow.RemoveDataConsumer(item) self.Items = [] + self.ResetVariableNameMask() def IsEmpty(self): return len(self.Items) == 0 @@ -412,6 +477,7 @@ else: self.ParentWindow.AddDataConsumer(iec_path, item) item.RefreshVariableType() + self.ResetVariableNameMask() def ResetData(self): for item in self.Items: @@ -428,7 +494,7 @@ for item in self.Items: new_id = wx.NewId() AppendMenu(menu, help='', id=new_id, kind=wx.ITEM_NORMAL, - text=item.GetVariable(20)) + text=item.GetVariable(self.VariableNameMask)) self.Bind(wx.EVT_MENU, self.GetForceVariableMenuFunction(item), id=new_id) @@ -443,7 +509,7 @@ for item in self.Items: new_id = wx.NewId() AppendMenu(menu, help='', id=new_id, kind=wx.ITEM_NORMAL, - text=item.GetVariable(20)) + text=item.GetVariable(self.VariableNameMask)) self.Bind(wx.EVT_MENU, self.GetReleaseVariableMenuFunction(item), id=new_id) @@ -459,7 +525,7 @@ for item in self.Items: new_id = wx.NewId() AppendMenu(menu, help='', id=new_id, kind=wx.ITEM_NORMAL, - text=item.GetVariable(20)) + text=item.GetVariable(self.VariableNameMask)) self.Bind(wx.EVT_MENU, self.GetDeleteValueMenuFunction(item), id=new_id) @@ -545,12 +611,22 @@ self.ValueLabel.SetForegroundColour(wx.BLACK) self.ValueLabel.SetSelection(self.ValueLabel.GetLastPosition(), -1) + def compute_mask(x, y): + mask = [] + for xp, yp in zip(x, y): + if xp == yp: + mask.append(xp) + else: + mask.append("*") + return mask + class DebugVariableGraphic(DebugVariableViewer): def __init__(self, parent, window, items, graph_type): DebugVariableViewer.__init__(self, parent, window, items) self.GraphType = graph_type + self.CursorTick = None self.ResetGraphics() @@ -560,6 +636,7 @@ self.Canvas = FigureCanvas(self, -1, self.Figure) self.Canvas.SetMinSize(wx.Size(200, 200)) self.Canvas.SetDropTarget(DebugVariableDropTarget(self.ParentWindow, self)) + self.Canvas.mpl_connect('motion_notify_event', self.OnCanvasMotion) self.Canvas.Bind(wx.EVT_LEFT_DOWN, self.OnCanvasClick) self.MainSizer.AddWindow(self.Canvas, flag=wx.GROW) @@ -607,18 +684,41 @@ event.Skip() def DoDragDrop(self, item_idx): + self.ParentWindow.ResetCursorTickRatio() data = wx.TextDataObject(str((self.Items[item_idx].GetVariable(), "debug", "move"))) dragSource = wx.DropSource(self.Canvas) dragSource.SetData(data) dragSource.DoDragDrop() - - def OnMotion(self, event): + + def OnAxesMotion(self, event): if self.Is3DCanvas(): current_time = gettime() if current_time - self.LastMotionTime > REFRESH_PERIOD: self.LastMotionTime = current_time Axes3D._on_move(self.Axes, event) + def OnCanvasMotion(self, event): + if not self.Is3DCanvas(): + if event.inaxes == self.Axes: + start_tick, end_tick = self.ParentWindow.GetRange() + cursor_tick_ratio = None + if self.GraphType == GRAPH_ORTHOGONAL: + x_data = self.Items[0].GetData(start_tick, end_tick) + y_data = self.Items[1].GetData(start_tick, end_tick) + 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) + cursor_tick_ratio = float(x_data[numpy.argmin(d), 0] - start_tick) / (end_tick - start_tick) + else: + data = self.Items[0].GetData(start_tick, end_tick) + if len(data) > 0: + x_min, x_max = self.Axes.get_xlim() + cursor_tick_ratio = float(event.xdata - x_min) / (x_max - x_min) + if cursor_tick_ratio is not None: + self.ParentWindow.SetCursorTickRatio(cursor_tick_ratio) + else: + self.ParentWindow.ResetCursorTickRatio() + def OnSplitButton(self, event): if len(self.Items) == 2 or self.GraphType == GRAPH_ORTHOGONAL: wx.CallAfter(self.ParentWindow.SplitGraphs, self) @@ -627,7 +727,7 @@ for item in self.Items: new_id = wx.NewId() AppendMenu(menu, help='', id=new_id, kind=wx.ITEM_NORMAL, - text=item.GetVariable(20)) + text=item.GetVariable(self.VariableNameMask)) self.Bind(wx.EVT_MENU, self.GetSplitGraphMenuFunction(item), id=new_id) @@ -645,15 +745,19 @@ self.Axes = self.Figure.gca(projection='3d') self.Axes.set_color_cycle(['b']) self.LastMotionTime = gettime() - setattr(self.Axes, "_on_move", self.OnMotion) + setattr(self.Axes, "_on_move", self.OnAxesMotion) self.Axes.mouse_init() else: self.Axes = self.Figure.gca() if self.GraphType == GRAPH_ORTHOGONAL: self.Figure.subplotpars.update(bottom=0.15) + self.Axes.set_title('.'.join(self.VariableNameMask)) self.Plots = [] + self.VLine = None + self.HLine = None + self.TickLabel = None self.SplitButton.Enable(len(self.Items) > 1) - + def AddItem(self, item): DebugVariableViewer.AddItem(self, item) self.ResetGraphics() @@ -661,16 +765,21 @@ def RemoveItem(self, item): DebugVariableViewer.RemoveItem(self, item) if not self.IsEmpty(): + self.ResetVariableNameMask() self.ResetGraphics() def UnregisterObsoleteData(self): DebugVariableViewer.UnregisterObsoleteData(self) if not self.IsEmpty(): + self.ResetVariableNameMask() self.ResetGraphics() def Is3DCanvas(self): return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3 + def SetCursorTick(self, cursor_tick): + self.CursorTick = cursor_tick + def Refresh(self, refresh_graphics=True): if refresh_graphics: @@ -707,6 +816,23 @@ x_min, x_max = start_tick, end_tick y_min, y_max = y_center - y_range * 0.55, y_center + y_range * 0.55 + if self.CursorTick is not None: + if self.VLine is None: + self.VLine = self.Axes.axvline(self.CursorTick, color='r') + else: + self.VLine.set_xdata((self.CursorTick, self.CursorTick)) + self.VLine.set_visible(True) + tick_label = self.ParentWindow.GetTickLabel(self.CursorTick) + if self.TickLabel is None: + self.TickLabel = self.Axes.text(0.5, 0.05, tick_label, + size = 'small', transform = self.Axes.transAxes) + else: + self.TickLabel.set_text(tick_label) + else: + if self.VLine is not None: + self.VLine.set_visible(False) + if self.TickLabel is not None: + self.TickLabel.set_text("") else: min_start_tick = reduce(max, [item.GetData()[0, 0] for item in self.Items @@ -715,6 +841,9 @@ end_tick = max(end_tick, min_start_tick) x_data, x_min, x_max = OrthogonalData(self.Items[0], start_tick, end_tick) y_data, y_min, y_max = OrthogonalData(self.Items[1], start_tick, end_tick) + if self.CursorTick is not None: + x_cursor, x_forced = self.Items[0].GetValue(self.CursorTick, raw=True) + y_cursor, y_forced = self.Items[1].GetValue(self.CursorTick, raw=True) length = 0 if x_data is not None and y_data is not None: length = min(len(x_data), len(y_data)) @@ -728,23 +857,64 @@ self.Plots[0].set_data( x_data[:, 1][:length], y_data[:, 1][:length]) + + if self.CursorTick is not None: + if self.VLine is None: + self.VLine = self.Axes.axvline(x_cursor, color='r') + else: + self.VLine.set_xdata((x_cursor, x_cursor)) + if self.HLine is None: + self.HLine = self.Axes.axhline(y_cursor, color='r') + else: + self.HLine.set_ydata((y_cursor, y_cursor)) + self.VLine.set_visible(True) + self.HLine.set_visible(True) + tick_label = self.ParentWindow.GetTickLabel(self.CursorTick) + if self.TickLabel is None: + self.TickLabel = self.Axes.text(0.05, 0.90, tick_label, + size = 'small', transform = self.Axes.transAxes) + else: + self.TickLabel.set_text(tick_label) + else: + if self.VLine is not None: + self.VLine.set_visible(False) + if self.HLine is not None: + self.HLine.set_visible(False) + if self.TickLabel is not None: + self.TickLabel.set_text("") else: while len(self.Axes.lines) > 0: self.Axes.lines.pop() z_data, z_min, z_max = OrthogonalData(self.Items[2], start_tick, end_tick) + if self.CursorTick is not None: + z_cursor, z_forced = self.Items[2].GetValue(self.CursorTick, raw=True) if x_data is not None and y_data is not None and z_data is not None: length = min(length, len(z_data)) self.Axes.plot(x_data[:, 1][:length], y_data[:, 1][:length], zs = z_data[:, 1][:length]) self.Axes.set_zlim(z_min, z_max) - + if self.CursorTick is not None: + 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"] = 'r' + self.Axes.plot(**kwargs) + self.Axes.set_xlim(x_min, x_max) self.Axes.set_ylim(y_min, y_max) - labels = ["%s: %s" % (item.GetVariable(40), item.GetValue()) - for item in self.Items] - colors = [{True: 'b', False: 'k'}[item.IsForced()] for item in self.Items] + if self.CursorTick is not None: + values, forced = apply(zip, [item.GetValue(self.CursorTick) for item in self.Items]) + else: + values, forced = apply(zip, [(item.GetValue(), item.IsForced()) for item in self.Items]) + names = [item.GetVariable(self.VariableNameMask) for item in self.Items] + labels = map(lambda x: "%s: %s" % x, zip(names, values)) + colors = map(lambda x: {True: 'b', False: 'k'}[x], forced) if self.GraphType == GRAPH_PARALLEL: self.Legend = self.Axes.legend(self.Plots, labels, loc="upper left", frameon=False, @@ -786,6 +956,7 @@ self.StartTick = 0 self.Fixed = False self.Force = False + self.GraphicPanels = [] graphics_button_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -834,6 +1005,7 @@ self.GraphicsWindow.SetSizer(self.GraphicsSizer) self.RefreshCanvasRange() + self.CursorTickRatio = None else: main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0) @@ -923,8 +1095,13 @@ self.Ticks = numpy.append(self.Ticks, [tick]) if not self.Fixed or tick < self.StartTick + self.CurrentRange: self.StartTick = max(self.StartTick, tick - self.CurrentRange) + self.ResetCursorTick(False) DebugViewer.NewDataAvailable(self, tick, *args, **kwargs) + def ForceRefresh(self): + self.Force = True + wx.CallAfter(self.NewDataAvailable, None, True) + def RefreshGraphicsSizer(self): self.GraphicsSizer.Clear() @@ -934,6 +1111,45 @@ self.GraphicsSizer.Layout() self.RefreshGraphicsWindowScrollbars() + def SetCursorTickRatio(self, cursor_tick_ratio): + self.CursorTickRatio = cursor_tick_ratio + self.ResetCursorTick() + + def ResetCursorTickRatio(self): + self.CursorTickRatio = None + self.ResetCursorTick() + + def ResetCursorTick(self, force_refresh=True): + if self.CursorTickRatio is not None and len(self.Ticks) > 0: + raw_tick = self.StartTick + self.CursorTickRatio * self.CurrentRange + cursor_tick = self.Ticks[numpy.argmin(abs(self.Ticks - raw_tick))] + else: + cursor_tick = None + for panel in self.GraphicPanels: + if isinstance(panel, DebugVariableGraphic): + panel.SetCursorTick(cursor_tick) + if force_refresh: + self.ForceRefresh() + + def GetTickLabel(self, tick): + label = "Tick: %d" % tick + if self.Ticktime > 0: + tick_duration = int(tick * self.Ticktime) + not_null = False + duration = "" + for value, format in [(tick_duration / DAY, "%dd"), + ((tick_duration % DAY) / HOUR, "%dh"), + ((tick_duration % HOUR) / MINUTE, "%dm"), + ((tick_duration % MINUTE) / SECOND, "%ds")]: + + if value > 0 or not_null: + duration += format % value + not_null = True + + duration += "%gms" % (float(tick_duration % SECOND) / MILLISECOND) + label += "(%s)" % duration + return label + def RefreshView(self, only_values=False): if USE_MPL: self.RefreshCanvasPosition() @@ -972,6 +1188,14 @@ for panel in self.GraphicPanels: panel.UnregisterObsoleteData() + if panel.IsEmpty(): + if panel.Canvas.HasCapture(): + panel.Canvas.ReleaseMouse() + self.GraphicPanels.remove(panel) + panel.Destroy() + + self.RefreshGraphicsSizer() + self.ForceRefresh() else: items = [(idx, item) for idx, item in enumerate(self.Table.GetData())] @@ -1086,8 +1310,7 @@ self.StartTick = min(self.StartTick, self.Ticks[-1] - self.CurrentRange) else: self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) - self.Force = True - self.RefreshView(True) + self.ForceRefresh() def OnRangeChanged(self, event): try: @@ -1105,26 +1328,33 @@ self.Fixed = False for panel in self.GraphicPanels: panel.ResetData() - self.RefreshView(True) + self.ForceRefresh() event.Skip() def OnCurrentButton(self, event): if len(self.Ticks) > 0: self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) self.Fixed = False - self.Force = True - self.RefreshView(True) + self.ForceRefresh() event.Skip() def CopyDataToClipboard(self, variables): - text = "tick;%s;\n" % ";".join([var_name for var_name, data in variables]) + text = "tick;%s;\n" % ";".join([item.GetVariable() for item, data in variables]) next_tick = NextTick(variables) while next_tick is not None: values = [] - for var_name, data in variables: + for item, data in variables: if len(data) > 0: if next_tick == data[0][0]: - values.append("%.3f" % data.pop(0)[1]) + var_type = item.GetVariableType() + if var_type in ["STRING", "WSTRING"]: + value = item.GetRawValue(int(data.pop(0)[2])) + if var_type == "STRING": + values.append("'%s'" % value) + else: + values.append('"%s"' % value) + else: + values.append("%.3f" % data.pop(0)[1]) else: values.append("") else: @@ -1135,9 +1365,15 @@ def OnExportGraphButton(self, event): variables = [] - for item in self.Table.GetData(): + if USE_MPL: + items = [] + for panel in self.GraphicPanels: + items.extend(panel.GetItems()) + else: + items = self.Table.GetData() + for item in items: if item.IsNumVariable(): - variables.append((item.GetVariable(), [entry for entry in item.GetData()])) + variables.append((item, [entry for entry in item.GetData()])) wx.CallAfter(self.CopyDataToClipboard, variables) event.Skip() @@ -1145,8 +1381,7 @@ if len(self.Ticks) > 0: self.StartTick = self.Ticks[0] + event.GetPosition() self.Fixed = True - self.Force = True - wx.CallAfter(self.NewDataAvailable, None, True) + self.ForceRefresh() event.Skip() def GetRange(self): @@ -1187,7 +1422,7 @@ else: self.Table.InsertItem(idx, item) - self.RefreshView() + self.ForceRefresh() def MoveGraph(self, iec_path, idx = None): if idx is None: @@ -1210,7 +1445,7 @@ panel = DebugVariableGraphic(self.GraphicsWindow, self, [item], GRAPH_PARALLEL) self.GraphicPanels.insert(idx, panel) self.RefreshGraphicsSizer() - self.RefreshView() + self.ForceRefresh() def SplitGraphs(self, source_panel, item=None): source_idx = self.GetViewerIndex(source_panel) @@ -1238,7 +1473,7 @@ self.GraphicPanels.insert(source_idx + 1, panel) self.RefreshGraphicsSizer() - self.RefreshView() + self.ForceRefresh() def MergeGraphs(self, source, target_idx, merge_type, force=False): source_item = None @@ -1277,7 +1512,7 @@ target_panel.ResetGraphics() self.RefreshGraphicsSizer() - self.RefreshView() + self.ForceRefresh() def DeleteValue(self, source_panel, item=None): source_idx = self.GetViewerIndex(source_panel) @@ -1294,7 +1529,7 @@ source_panel.Destroy() self.GraphicPanels.remove(source_panel) self.RefreshGraphicsSizer() - self.RefreshView() + self.ForceRefresh() def GetDebugVariables(self): if USE_MPL: @@ -1323,7 +1558,7 @@ self.RefreshGraphicsSizer() else: self.InsertValue(variable, force=True) - self.RefreshView() + self.ForceRefresh() else: for variable in variables: if isinstance(variable, (ListType, TupleType)): @@ -1351,4 +1586,4 @@ def OnGraphicsWindowResize(self, event): self.RefreshGraphicsWindowScrollbars() - event.Skip() \ No newline at end of file + event.Skip()