printrun-src/printrun/gui/graph.py

changeset 46
cce0af6351f0
parent 15
0bbb006204fc
equal deleted inserted replaced
45:c82943fb205f 46:cce0af6351f0
1 #!/usr/bin/env python
2
3 # This file is part of the Printrun suite. 1 # This file is part of the Printrun suite.
4 # 2 #
5 # Printrun is free software: you can redistribute it and/or modify 3 # Printrun is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by 4 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or 5 # the Free Software Foundation, either version 3 of the License, or
15 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
16 # along with Printrun. If not, see <http://www.gnu.org/licenses/>. 14 # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
17 15
18 import wx 16 import wx
19 from math import log10, floor, ceil 17 from math import log10, floor, ceil
18 from bisect import bisect_left
20 19
21 from printrun.utils import install_locale 20 from printrun.utils import install_locale
22 install_locale('pronterface') 21 install_locale('pronterface')
23 22
24 from .bufferedcanvas import BufferedCanvas 23 from .bufferedcanvas import BufferedCanvas
25 24
26 class GraphWindow(wx.Frame): 25 class GraphWindow(wx.Frame):
27 def __init__(self, root, parent_graph = None, size = (600, 600)): 26 def __init__(self, root, parent_graph = None, size = (600, 600)):
28 super(GraphWindow, self).__init__(None, title = _("Temperature graph"), 27 super().__init__(None, title = _("Temperature graph"),
29 size = size) 28 size = size)
30 panel = wx.Panel(self, -1) 29 self.parentg = parent_graph
30 panel = wx.Panel(self)
31 vbox = wx.BoxSizer(wx.VERTICAL) 31 vbox = wx.BoxSizer(wx.VERTICAL)
32 self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph) 32 self.graph = Graph(panel, wx.ID_ANY, root, parent_graph = parent_graph)
33 vbox.Add(self.graph, 1, wx.EXPAND) 33 vbox.Add(self.graph, 1, wx.EXPAND)
34 panel.SetSizer(vbox) 34 panel.SetSizer(vbox)
35 35
36 def Destroy(self):
37 self.graph.StopPlotting()
38 if self.parentg is not None:
39 self.parentg.window=None
40 return super().Destroy()
41
42 def __del__(self):
43 if self.parentg is not None:
44 self.parentg.window=None
45 self.graph.StopPlotting()
46
36 class Graph(BufferedCanvas): 47 class Graph(BufferedCanvas):
37 '''A class to show a Graph with Pronterface.''' 48 '''A class to show a Graph with Pronterface.'''
38 49
39 def __init__(self, parent, id, root, pos = wx.DefaultPosition, 50 def __init__(self, parent, id, root, pos = wx.DefaultPosition,
40 size = wx.Size(150, 80), style = 0, parent_graph = None): 51 size = wx.Size(150, 80), style = 0, parent_graph = None):
41 # Forcing a no full repaint to stop flickering 52 # Forcing a no full repaint to stop flickering
42 style = style | wx.NO_FULL_REPAINT_ON_RESIZE 53 style = style | wx.NO_FULL_REPAINT_ON_RESIZE
43 super(Graph, self).__init__(parent, id, pos, size, style) 54 super().__init__(parent, id, pos, size, style)
44 self.root = root 55 self.root = root
45 56
46 if parent_graph is not None: 57 if parent_graph is not None:
47 self.extruder0temps = parent_graph.extruder0temps 58 self.extruder0temps = parent_graph.extruder0temps
48 self.extruder0targettemps = parent_graph.extruder0targettemps 59 self.extruder0targettemps = parent_graph.extruder0targettemps
49 self.extruder1temps = parent_graph.extruder1temps 60 self.extruder1temps = parent_graph.extruder1temps
50 self.extruder1targettemps = parent_graph.extruder1targettemps 61 self.extruder1targettemps = parent_graph.extruder1targettemps
51 self.bedtemps = parent_graph.bedtemps 62 self.bedtemps = parent_graph.bedtemps
52 self.bedtargettemps = parent_graph.bedtargettemps 63 self.bedtargettemps = parent_graph.bedtargettemps
53 self.fanpowers=parent_graph.fanpowers 64 self.fanpowers=parent_graph.fanpowers
54 else: 65 else:
55 self.extruder0temps = [0] 66 self.extruder0temps = [0]
56 self.extruder0targettemps = [0] 67 self.extruder0targettemps = [0]
57 self.extruder1temps = [0] 68 self.extruder1temps = [0]
58 self.extruder1targettemps = [0] 69 self.extruder1targettemps = [0]
59 self.bedtemps = [0] 70 self.bedtemps = [0]
60 self.bedtargettemps = [0] 71 self.bedtargettemps = [0]
61 self.fanpowers= [0] 72 self.fanpowers= [0]
62 73
63 self.timer = wx.Timer(self) 74 self.timer = wx.Timer(self)
64 self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer) 75 self.Bind(wx.EVT_TIMER, self.updateTemperatures, self.timer)
76 self.Bind(wx.EVT_WINDOW_DESTROY, self.processDestroy)
65 77
66 self.minyvalue = 0 78 self.minyvalue = 0
67 self.maxyvalue = 260 79 self.maxyvalue = 260
68 self.rescaley = True # should the Y axis be rescaled dynamically? 80 self.rescaley = True # should the Y axis be rescaled dynamically?
69 if self.rescaley: 81 if self.rescaley:
74 self.ybars = 5 86 self.ybars = 5
75 self.xbars = 7 # One bar per 10 second 87 self.xbars = 7 # One bar per 10 second
76 self.xsteps = 60 # Covering 1 minute in the graph 88 self.xsteps = 60 # Covering 1 minute in the graph
77 89
78 self.window = None 90 self.window = None
91 self.reserved = []
92
93 def processDestroy(self, event):
94 # print('processDestroy')
95 self.StopPlotting()
96 self.Unbind(wx.EVT_TIMER)
97 event.Skip()
79 98
80 def show_graph_window(self, event = None): 99 def show_graph_window(self, event = None):
81 if not self.window: 100 if self.window is None or not self.window:
82 self.window = GraphWindow(self.root, self) 101 self.window = GraphWindow(self.root, self)
83 self.window.Show() 102 self.window.Show()
84 if self.timer.IsRunning(): 103 if self.timer.IsRunning():
85 self.window.graph.StartPlotting(self.timer.Interval) 104 self.window.graph.StartPlotting(self.timer.Interval)
86 else: 105 else:
88 107
89 def __del__(self): 108 def __del__(self):
90 if self.window: self.window.Close() 109 if self.window: self.window.Close()
91 110
92 def updateTemperatures(self, event): 111 def updateTemperatures(self, event):
112 # print('updateTemperatures')
93 self.AddBedTemperature(self.bedtemps[-1]) 113 self.AddBedTemperature(self.bedtemps[-1])
94 self.AddBedTargetTemperature(self.bedtargettemps[-1]) 114 self.AddBedTargetTemperature(self.bedtargettemps[-1])
95 self.AddExtruder0Temperature(self.extruder0temps[-1]) 115 self.AddExtruder0Temperature(self.extruder0temps[-1])
96 self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1]) 116 self.AddExtruder0TargetTemperature(self.extruder0targettemps[-1])
97 self.AddExtruder1Temperature(self.extruder1temps[-1]) 117 self.AddExtruder1Temperature(self.extruder1temps[-1])
98 self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1]) 118 self.AddExtruder1TargetTemperature(self.extruder1targettemps[-1])
99 self.AddFanPower(self.fanpowers[-1]) 119 self.AddFanPower(self.fanpowers[-1])
100 if self.rescaley: 120 if self.rescaley:
101 self._ybounds.update() 121 self._ybounds.update()
102 self.Refresh() 122 self.Refresh()
103 123
104 def drawgrid(self, dc, gc): 124 def drawgrid(self, dc, gc):
122 font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD) 142 font = wx.Font(10, wx.DEFAULT, wx.NORMAL, wx.BOLD)
123 gc.SetFont(font, wx.Colour(23, 44, 44)) 143 gc.SetFont(font, wx.Colour(23, 44, 44))
124 144
125 # draw vertical bars 145 # draw vertical bars
126 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1)) 146 dc.SetPen(wx.Pen(wx.Colour(225, 225, 225), 1))
147 xscale = float(self.width - 1) / (self.xbars - 1)
127 for x in range(self.xbars + 1): 148 for x in range(self.xbars + 1):
128 dc.DrawLine(x * (float(self.width - 1) / (self.xbars - 1)), 149 x = x * xscale
129 0, 150 dc.DrawLine(x, 0, x, self.height)
130 x * (float(self.width - 1) / (self.xbars - 1)),
131 self.height)
132 151
133 # draw horizontal bars 152 # draw horizontal bars
134 spacing = self._calculate_spacing() # spacing between bars, in degrees 153 spacing = self._calculate_spacing() # spacing between bars, in degrees
135 yspan = self.maxyvalue - self.minyvalue 154 yspan = self.maxyvalue - self.minyvalue
136 ybars = int(yspan / spacing) # Should be close to self.ybars 155 ybars = int(yspan / spacing) # Should be close to self.ybars
139 for y in range(firstbar, firstbar + ybars + 1): 158 for y in range(firstbar, firstbar + ybars + 1):
140 # y_pos = y*(float(self.height)/self.ybars) 159 # y_pos = y*(float(self.height)/self.ybars)
141 degrees = y * spacing 160 degrees = y * spacing
142 y_pos = self._y_pos(degrees) 161 y_pos = self._y_pos(degrees)
143 dc.DrawLine(0, y_pos, self.width, y_pos) 162 dc.DrawLine(0, y_pos, self.width, y_pos)
144 gc.DrawText(unicode(y * spacing), 163 label = str(y * spacing)
145 1, y_pos - (font.GetPointSize() / 2)) 164 label_y = y_pos - font.GetPointSize() / 2
146 165 self.layoutText(label, 1, label_y, gc)
147 if self.timer.IsRunning() is False: 166 gc.DrawText(label, 1, label_y)
167
168 if not self.timer.IsRunning():
148 font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD) 169 font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.BOLD)
149 gc.SetFont(font, wx.Colour(3, 4, 4)) 170 gc.SetFont(font, wx.Colour(3, 4, 4))
150 gc.DrawText("Graph offline", 171 gc.DrawText("Graph offline",
151 self.width / 2 - (font.GetPointSize() * 3), 172 self.width / 2 - font.GetPointSize() * 3,
152 self.height / 2 - (font.GetPointSize() * 1)) 173 self.height / 2 - font.GetPointSize() * 1)
153 174
154 # dc.DrawCircle(50, 50, 1) 175 # dc.DrawCircle(50, 50, 1)
155 176
156 # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1)) 177 # gc.SetPen(wx.Pen(wx.Colour(255, 0, 0, 0), 1))
157 # gc.DrawLines([[20, 30], [10, 53]]) 178 # gc.DrawLines([[20, 30], [10, 53]])
185 else: 206 else:
186 return 10 ** (exponent + 1) 207 return 10 ** (exponent + 1)
187 208
188 def drawtemperature(self, dc, gc, temperature_list, 209 def drawtemperature(self, dc, gc, temperature_list,
189 text, text_xoffset, r, g, b, a): 210 text, text_xoffset, r, g, b, a):
190 if self.timer.IsRunning() is False: 211 color = self.timer.IsRunning() and (r, g, b, a) or [128] * 4
191 dc.SetPen(wx.Pen(wx.Colour(128, 128, 128, 128), 1)) 212 dc.SetPen(wx.Pen(color, 1))
192 else:
193 dc.SetPen(wx.Pen(wx.Colour(r, g, b, a), 1))
194 213
195 x_add = float(self.width) / self.xsteps 214 x_add = float(self.width) / self.xsteps
196 x_pos = 0.0 215 x_pos = 0.0
197 lastxvalue = 0.0 216 lastxvalue = 0.0
198 lastyvalue = temperature_list[-1] 217 lastyvalue = temperature_list[-1]
199 218
200 for temperature in (temperature_list): 219 for temperature in temperature_list:
201 y_pos = self._y_pos(temperature) 220 y_pos = self._y_pos(temperature)
202 if (x_pos > 0.0): # One need 2 points to draw a line. 221 if x_pos > 0: # One need 2 points to draw a line.
203 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos) 222 dc.DrawLine(lastxvalue, lastyvalue, x_pos, y_pos)
204 223
205 lastxvalue = x_pos 224 lastxvalue = x_pos
206 x_pos = float(x_pos) + x_add 225 x_pos += x_add
207 lastyvalue = y_pos 226 lastyvalue = y_pos
208 227
209 if len(text) > 0: 228 if text:
210 font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD) 229 font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.BOLD)
211 # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL) 230 # font = wx.Font(8, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
212 if self.timer.IsRunning() is False: 231 gc.SetFont(font, color[:3])
213 gc.SetFont(font, wx.Colour(128, 128, 128))
214 else:
215 gc.SetFont(font, wx.Colour(r, g, b))
216 232
217 text_size = len(text) * text_xoffset + 1 233 text_size = len(text) * text_xoffset + 1
218 gc.DrawText(text, 234 pos = self.layoutText(text, lastxvalue, lastyvalue, gc)
219 x_pos - x_add - (font.GetPointSize() * text_size), 235 gc.DrawText(text, pos.x, pos.y)
220 lastyvalue - (font.GetPointSize() / 2)) 236
237 def layoutRect(self, rc):
238 res = LtRect(rc)
239 reserved = sorted((rs for rs in self.reserved
240 if not (rc.bottom < rs.top or rc.top > rs.bottom)),
241 key=wx.Rect.GetLeft)
242 self.boundRect(res)
243 # search to the left for gaps large enough to accomodate res
244 rci = bisect_left(reserved, res)
245
246 for i in range(rci, len(reserved)-1):
247 res.x = reserved[i].right + 1
248 if res.right < reserved[i+1].left:
249 #found good res
250 break
251 else:
252 # did not find gap to the right
253 if reserved:
254 #try to respect rc.x at the cost of a gap (50...Bed)
255 if res.left < reserved[-1].right:
256 res.x = reserved[-1].right + 1
257 if res.right >= self.width:
258 #goes beyond window bounds
259 # try to the left
260 for i in range(min(rci, len(reserved)-1), 0, -1):
261 res.x = reserved[i].left - rc.width
262 if reserved[i-1].right < res.left:
263 break
264 else:
265 res = LtRect(self.layoutRectY(rc))
266
267 self.reserved.append(res)
268 return res
269
270 def boundRect(self, rc):
271 rc.x = min(rc.x, self.width - rc.width)
272 return rc
273
274 def layoutRectY(self, rc):
275 top = self.height
276 bottom = 0
277 collision = False
278 res = LtRect(rc)
279 res.x = max(self.gridLabelsRight+1, min(rc.x, self.width-rc.width))
280 for rs in self.reserved:
281 if not (res.right < rs.left or res.left > rs.right):
282 collision = True
283 top = min(top, rs.Top)
284 bottom = max(bottom, rs.bottom)
285 if collision:
286 res.y = top - rc.height
287 if res.y < 0:
288 res.y = bottom+1
289 if res.bottom >= self.height:
290 res.y = rc.y
291 return res
292
293 def layoutText(self, text, x, y, gc):
294 ext = gc.GetTextExtent(text)
295 rc = self.layoutRect(wx.Rect(x, y, *ext))
296 # print('layoutText', text, rc.TopLeft)
297 return rc
221 298
222 def drawfanpower(self, dc, gc): 299 def drawfanpower(self, dc, gc):
223 self.drawtemperature(dc, gc, self.fanpowers, 300 self.drawtemperature(dc, gc, self.fanpowers,
224 "Fan", 1, 0, 0, 0, 128) 301 "Fan", 1, 0, 0, 0, 128)
225 302
313 def StartPlotting(self, time): 390 def StartPlotting(self, time):
314 self.Refresh() 391 self.Refresh()
315 self.timer.Start(time) 392 self.timer.Start(time)
316 if self.window: self.window.graph.StartPlotting(time) 393 if self.window: self.window.graph.StartPlotting(time)
317 394
395 def Destroy(self):
396 # print(__class__, '.Destroy')
397 self.StopPlotting()
398 return super(BufferedCanvas, self).Destroy()
399
318 def StopPlotting(self): 400 def StopPlotting(self):
319 self.timer.Stop() 401 self.timer.Stop()
320 self.Refresh() 402 #self.Refresh() # do not refresh when stopping in case the underlying object has been destroyed already
321 if self.window: self.window.graph.StopPlotting() 403 if self.window: self.window.graph.StopPlotting()
322 404
323 def draw(self, dc, w, h): 405 def draw(self, dc, w, h):
324 dc.SetBackground(wx.Brush(self.root.bgcolor)) 406 dc.SetBackground(wx.Brush(self.root.settings.graph_color_background))
325 dc.Clear() 407 dc.Clear()
326 gc = wx.GraphicsContext.Create(dc) 408 gc = wx.GraphicsContext.Create(dc)
327 self.width = w 409 self.width = w
328 self.height = h 410 self.height = h
411
412 self.reserved.clear()
329 self.drawgrid(dc, gc) 413 self.drawgrid(dc, gc)
414 self.gridLabelsRight = self.reserved[-1].Right
415
330 self.drawbedtargettemp(dc, gc) 416 self.drawbedtargettemp(dc, gc)
331 self.drawbedtemp(dc, gc) 417 self.drawbedtemp(dc, gc)
332 self.drawfanpower(dc, gc) 418 self.drawfanpower(dc, gc)
333 self.drawextruder0targettemp(dc, gc) 419 self.drawextruder0targettemp(dc, gc)
334 self.drawextruder0temp(dc, gc) 420 self.drawextruder0temp(dc, gc)
335 self.drawextruder1targettemp(dc, gc) 421 if self.extruder1targettemps[-1]>0 or self.extruder1temps[-1]>5:
336 self.drawextruder1temp(dc, gc) 422 self.drawextruder1targettemp(dc, gc)
337 423 self.drawextruder1temp(dc, gc)
338 class _YBounds(object): 424
425 class _YBounds:
339 """Small helper class to claculate y bounds dynamically""" 426 """Small helper class to claculate y bounds dynamically"""
340 427
341 def __init__(self, graph, minimum_scale=5.0, buffer=0.10): 428 def __init__(self, graph, minimum_scale=5.0, buffer=0.10):
342 """_YBounds(Graph,float,float) 429 """_YBounds(Graph,float,float)
343 430
400 miny = min(miny, extruder1_min, extruder1_target) 487 miny = min(miny, extruder1_min, extruder1_target)
401 maxy = max(maxy, extruder1_max, extruder1_target) 488 maxy = max(maxy, extruder1_max, extruder1_target)
402 if bed_target > 0 or bed_max > 5: # use HBP 489 if bed_target > 0 or bed_max > 5: # use HBP
403 miny = min(miny, bed_min, bed_target) 490 miny = min(miny, bed_min, bed_target)
404 maxy = max(maxy, bed_max, bed_target) 491 maxy = max(maxy, bed_max, bed_target)
405 miny=min(0,miny); 492 miny = min(0, miny)
406 maxy=max(260,maxy); 493 maxy = max(260, maxy)
407 494
408 padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer) 495 padding = (maxy - miny) * self.buffer / (1.0 - 2 * self.buffer)
409 miny -= padding 496 miny -= padding
410 maxy += padding 497 maxy += padding
411 498
434 miny = min(miny, extruder1_min, extruder1_target) 521 miny = min(miny, extruder1_min, extruder1_target)
435 maxy = max(maxy, extruder1_max, extruder1_target) 522 maxy = max(maxy, extruder1_max, extruder1_target)
436 if bed_target > 0 or bed_max > 5: # use HBP 523 if bed_target > 0 or bed_max > 5: # use HBP
437 miny = min(miny, bed_min, bed_target) 524 miny = min(miny, bed_min, bed_target)
438 maxy = max(maxy, bed_max, bed_target) 525 maxy = max(maxy, bed_max, bed_target)
439 miny=min(0,miny); 526 miny = min(0, miny)
440 maxy=max(260,maxy); 527 maxy = max(260, maxy)
441 528
442 # We have to rescale, so add padding 529 # We have to rescale, so add padding
443 bufratio = self.buffer / (1.0 - self.buffer) 530 bufratio = self.buffer / (1.0 - self.buffer)
444 if miny < self.graph.minyvalue: 531 if miny < self.graph.minyvalue:
445 padding = (self.graph.maxyvalue - miny) * bufratio 532 padding = (self.graph.maxyvalue - miny) * bufratio
448 padding = (maxy - self.graph.minyvalue) * bufratio 535 padding = (maxy - self.graph.minyvalue) * bufratio
449 maxy += padding 536 maxy += padding
450 537
451 return (min(miny, self.graph.minyvalue), 538 return (min(miny, self.graph.minyvalue),
452 max(maxy, self.graph.maxyvalue)) 539 max(maxy, self.graph.maxyvalue))
540
541 class LtRect(wx.Rect):
542 def __lt__(self, other):
543 return self.x < other.x

mercurial