printrun-src/printrun/gui/graph.py

changeset 46
cce0af6351f0
parent 15
0bbb006204fc
--- a/printrun-src/printrun/gui/graph.py	Tue Jan 19 20:45:09 2021 +0100
+++ b/printrun-src/printrun/gui/graph.py	Wed Jan 20 10:15:13 2021 +0100
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-
 # This file is part of the Printrun suite.
 #
 # Printrun is free software: you can redistribute it and/or modify
@@ -17,6 +15,7 @@
 
 import wx
 from math import log10, floor, ceil
+from bisect import bisect_left
 
 from printrun.utils import install_locale
 install_locale('pronterface')
@@ -25,14 +24,26 @@
 
 class GraphWindow(wx.Frame):
     def __init__(self, root, parent_graph = None, size = (600, 600)):
-        super(GraphWindow, self).__init__(None, title = _("Temperature graph"),
+        super().__init__(None, title = _("Temperature graph"),
                                           size = size)
-        panel = wx.Panel(self, -1)
+        self.parentg = parent_graph
+        panel = wx.Panel(self)
         vbox = wx.BoxSizer(wx.VERTICAL)
         self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph)
         vbox.Add(self.graph, 1, wx.EXPAND)
         panel.SetSizer(vbox)
 
+    def Destroy(self):
+        self.graph.StopPlotting()
+        if self.parentg is not None:
+            self.parentg.window=None
+        return super().Destroy()
+
+    def __del__(self):
+        if self.parentg is not None:
+            self.parentg.window=None
+        self.graph.StopPlotting()
+
 class Graph(BufferedCanvas):
     '''A class to show a Graph with Pronterface.'''
 
@@ -40,7 +51,7 @@
                  size = wx.Size(150, 80), style = 0, parent_graph = None):
         # Forcing a no full repaint to stop flickering
         style = style | wx.NO_FULL_REPAINT_ON_RESIZE
-        super(Graph, self).__init__(parent, id, pos, size, style)
+        super().__init__(parent, id, pos, size, style)
         self.root = root
 
         if parent_graph is not None:
@@ -50,7 +61,7 @@
             self.extruder1targettemps = parent_graph.extruder1targettemps
             self.bedtemps = parent_graph.bedtemps
             self.bedtargettemps = parent_graph.bedtargettemps
-	    self.fanpowers=parent_graph.fanpowers
+            self.fanpowers=parent_graph.fanpowers
         else:
             self.extruder0temps = [0]
             self.extruder0targettemps = [0]
@@ -58,10 +69,11 @@
             self.extruder1targettemps = [0]
             self.bedtemps = [0]
             self.bedtargettemps = [0]
-	    self.fanpowers= [0]
+            self.fanpowers= [0]
 
         self.timer = wx.Timer(self)
         self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer)
+        self.Bind(wx.EVT_WINDOW_DESTROY, self.processDestroy)
 
         self.minyvalue = 0
         self.maxyvalue = 260
@@ -76,9 +88,16 @@
         self.xsteps = 60  # Covering 1 minute in the graph
 
         self.window = None
+        self.reserved = []
+
+    def processDestroy(self, event):
+        # print('processDestroy')
+        self.StopPlotting()
+        self.Unbind(wx.EVT_TIMER)
+        event.Skip()
 
     def show_graph_window(self, event = None):
-        if not self.window:
+        if self.window is None or not self.window:
             self.window = GraphWindow(self.root, self)
             self.window.Show()
             if self.timer.IsRunning():
@@ -90,13 +109,14 @@
         if self.window: self.window.Close()
 
     def updateTemperatures(self, event):
+        # print('updateTemperatures')
         self.AddBedTemperature(self.bedtemps[-1])
         self.AddBedTargetTemperature(self.bedtargettemps[-1])
         self.AddExtruder0Temperature(self.extruder0temps[-1])
         self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1])
         self.AddExtruder1Temperature(self.extruder1temps[-1])
         self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1])
-	self.AddFanPower(self.fanpowers[-1])
+        self.AddFanPower(self.fanpowers[-1])
         if self.rescaley:
             self._ybounds.update()
         self.Refresh()
@@ -124,11 +144,10 @@
 
         # draw vertical bars
         dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
+        xscale = float(self.width - 1) / (self.xbars - 1)
         for x in range(self.xbars + 1):
-            dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)),
-                        0,
-                        x * (float(self.width - 1) / (self.xbars - 1)),
-                        self.height)
+            x = x * xscale
+            dc.DrawLine(x, 0, x, self.height)
 
         # draw horizontal bars
         spacing = self._calculate_spacing()  # spacing between bars, in degrees
@@ -141,15 +160,17 @@
             degrees = y * spacing
             y_pos = self._y_pos(degrees)
             dc.DrawLine(0, y_pos, self.width, y_pos)
-            gc.DrawText(unicode(y * spacing),
-                        1, y_pos - (font.GetPointSize() / 2))
+            label = str(y * spacing)
+            label_y = y_pos - font.GetPointSize() / 2
+            self.layoutText(label, 1, label_y, gc)
+            gc.DrawText(label, 1, label_y)
 
-        if self.timer.IsRunning() is False:
+        if not self.timer.IsRunning():
             font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD)
             gc.SetFont(font, wx.Colour(3, 4, 4))
             gc.DrawText("Graph offline",
-                        self.width / 2 - (font.GetPointSize() * 3),
-                        self.height / 2 - (font.GetPointSize() * 1))
+                        self.width / 2 - font.GetPointSize() * 3,
+                        self.height / 2 - font.GetPointSize() * 1)
 
         # dc.DrawCircle(50, 50, 1)
 
@@ -187,37 +208,93 @@
 
     def drawtemperature(self, dc, gc, temperature_list,
                         text, text_xoffset, r, g, b, a):
-        if self.timer.IsRunning() is False:
-            dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1))
-        else:
-            dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1))
+        color = self.timer.IsRunning() and (r, g, b, a) or [128] * 4
+        dc.SetPen(wx.Pen(color, 1))
 
         x_add = float(self.width) / self.xsteps
         x_pos = 0.0
         lastxvalue = 0.0
         lastyvalue = temperature_list[-1]
 
-        for temperature in (temperature_list):
+        for temperature in temperature_list:
             y_pos = self._y_pos(temperature)
-            if (x_pos > 0.0):  # One need 2 points to draw a line.
+            if x_pos > 0:  # One need 2 points to draw a line.
                 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos)
 
             lastxvalue = x_pos
-            x_pos = float(x_pos) + x_add
+            x_pos += x_add
             lastyvalue = y_pos
 
-        if len(text) > 0:
+        if text:
             font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)
             # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
-            if self.timer.IsRunning() is False:
-                gc.SetFont(font, wx.Colour(128, 128, 128))
-            else:
-                gc.SetFont(font, wx.Colour(r, g, b))
+            gc.SetFont(font, color[:3])
 
             text_size = len(text) * text_xoffset + 1
-            gc.DrawText(text,
-                        x_pos - x_add - (font.GetPointSize() * text_size),
-                        lastyvalue - (font.GetPointSize() / 2))
+            pos = self.layoutText(text, lastxvalue, lastyvalue, gc)
+            gc.DrawText(text, pos.x, pos.y)
+
+    def layoutRect(self, rc):
+        res = LtRect(rc)
+        reserved = sorted((rs for rs in self.reserved
+            if not (rc.bottom < rs.top or rc.top > rs.bottom)),
+            key=wx.Rect.GetLeft)
+        self.boundRect(res)
+        # search to the left for gaps large enough to accomodate res
+        rci = bisect_left(reserved, res)
+
+        for i in range(rci, len(reserved)-1):
+            res.x = reserved[i].right + 1
+            if res.right < reserved[i+1].left:
+                #found good res
+                break
+        else:
+            # did not find gap to the right
+            if reserved:
+                #try to respect rc.x at the cost of a gap (50...Bed)
+                if res.left < reserved[-1].right:
+                    res.x = reserved[-1].right + 1
+                    if res.right >= self.width:
+                        #goes beyond window bounds
+                        # try to the left
+                        for i in range(min(rci, len(reserved)-1), 0, -1):
+                            res.x = reserved[i].left - rc.width
+                            if reserved[i-1].right < res.left:
+                                break
+                        else:
+                            res = LtRect(self.layoutRectY(rc))
+
+        self.reserved.append(res)
+        return res
+
+    def boundRect(self, rc):
+        rc.x = min(rc.x, self.width - rc.width)
+        return rc
+
+    def layoutRectY(self, rc):
+        top = self.height
+        bottom = 0
+        collision = False
+        res = LtRect(rc)
+        res.x = max(self.gridLabelsRight+1, min(rc.x, self.width-rc.width))
+        for rs in self.reserved:
+            if not (res.right < rs.left or res.left > rs.right):
+                collision = True
+                top = min(top, rs.Top)
+                bottom = max(bottom, rs.bottom)
+        if collision:
+            res.y = top - rc.height
+            if res.y < 0:
+                res.y = bottom+1
+                if res.bottom >= self.height:
+                    res.y = rc.y
+        return res
+
+    def layoutText(self, text, x, y, gc):
+        ext = gc.GetTextExtent(text)
+        rc = self.layoutRect(wx.Rect(x, y, *ext))
+        # print('layoutText', text, rc.TopLeft)
+        return rc
 
     def drawfanpower(self, dc, gc):
         self.drawtemperature(dc, gc, self.fanpowers,
@@ -315,27 +392,37 @@
         self.timer.Start(time)
         if self.window: self.window.graph.StartPlotting(time)
 
+    def Destroy(self):
+        # print(__class__, '.Destroy')
+        self.StopPlotting()
+        return super(BufferedCanvas, self).Destroy()
+
     def StopPlotting(self):
         self.timer.Stop()
-        self.Refresh()
+        #self.Refresh() # do not refresh when stopping in case the underlying object has been destroyed already
         if self.window: self.window.graph.StopPlotting()
 
     def draw(self, dc, w, h):
-        dc.SetBackground(wx.Brush(self.root.bgcolor))
+        dc.SetBackground(wx.Brush(self.root.settings.graph_color_background))
         dc.Clear()
         gc = wx.GraphicsContext.Create(dc)
         self.width = w
         self.height = h
+
+        self.reserved.clear()
         self.drawgrid(dc, gc)
+        self.gridLabelsRight = self.reserved[-1].Right
+
         self.drawbedtargettemp(dc, gc)
         self.drawbedtemp(dc, gc)
         self.drawfanpower(dc, gc)
         self.drawextruder0targettemp(dc, gc)
         self.drawextruder0temp(dc, gc)
-        self.drawextruder1targettemp(dc, gc)
-        self.drawextruder1temp(dc, gc)
+        if self.extruder1targettemps[-1]>0 or self.extruder1temps[-1]>5:
+            self.drawextruder1targettemp(dc, gc)
+            self.drawextruder1temp(dc, gc)
 
-    class _YBounds(object):
+    class _YBounds:
         """Small helper class to claculate y bounds dynamically"""
 
         def __init__(self, graph, minimum_scale=5.0, buffer=0.10):
@@ -402,8 +489,8 @@
             if bed_target > 0 or bed_max > 5:  # use HBP
                 miny = min(miny, bed_min, bed_target)
                 maxy = max(maxy, bed_max, bed_target)
-	    miny=min(0,miny);
-	    maxy=max(260,maxy);
+            miny = min(0, miny)
+            maxy = max(260, maxy)
 
             padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer)
             miny -= padding
@@ -436,8 +523,8 @@
             if bed_target > 0 or bed_max > 5:  # use HBP
                 miny = min(miny, bed_min, bed_target)
                 maxy = max(maxy, bed_max, bed_target)
-	    miny=min(0,miny);
-	    maxy=max(260,maxy);
+            miny = min(0, miny)
+            maxy = max(260, maxy)
 
             # We have to rescale, so add padding
             bufratio = self.buffer / (1.0 - self.buffer)
@@ -450,3 +537,7 @@
 
             return (min(miny, self.graph.minyvalue),
                     max(maxy, self.graph.maxyvalue))
+
+class LtRect(wx.Rect):
+    def __lt__(self, other):
+        return self.x < other.x

mercurial