summaryrefslogtreecommitdiffstats
path: root/unit_trace/viz/canvas.py
diff options
context:
space:
mode:
authorGary Bressler <garybressler@nc.rr.com>2010-04-06 12:45:04 -0400
committerGary Bressler <garybressler@nc.rr.com>2010-04-06 12:45:04 -0400
commitc7e3aaebdba7bf880534abd91a383b5543cf0be4 (patch)
tree048977efdaaa3d60e93c3d21ba29c46a0bfe71c3 /unit_trace/viz/canvas.py
parent7fdb4dbbbca577efbeec47cd1364eb319346a0cc (diff)
Making sure everything committed
Diffstat (limited to 'unit_trace/viz/canvas.py')
-rw-r--r--unit_trace/viz/canvas.py809
1 files changed, 809 insertions, 0 deletions
diff --git a/unit_trace/viz/canvas.py b/unit_trace/viz/canvas.py
new file mode 100644
index 0000000..758dea3
--- /dev/null
+++ b/unit_trace/viz/canvas.py
@@ -0,0 +1,809 @@
1#!/usr/bin/python
2
3"""Classes related to the drawing and area-selection primitives. Note that
4this file is quite low-level, in that its objects are mostly restricted to
5dealing with drawing the components of a real-time graph given coordinates
6rather than having an abstract knowledge of the graph's measurements or
7any information about events."""
8
9import math
10import cairo
11import os
12import copy
13
14import util
15from format import *
16
17def snap(pos):
18 """Takes in an x- or y-coordinate ``pos'' and snaps it to the pixel grid.
19 This is necessary because integer coordinates in Cairo actually denote
20 the spaces between pixels, not the pixels themselves, so if we draw a
21 line of width 1 on integer coordinates, it will come out blurry unless we shift it,
22 since the line will get distributed over two pixels. We actually apply this to all
23 coordinates to make sure everything is aligned."""
24 return pos - 0.5
25
26class Surface(object):
27 def __init__(self, fname='temp', ctx=None):
28 self.virt_x = 0
29 self.virt_y = 0
30 self.surface = None
31 self.width = 0
32 self.height = 0
33 self.scale = 1.0
34 self.fname = fname
35 self.ctx = ctx
36
37 def renew(self, width, height):
38 raise NotImplementedError
39
40 def change_ctx(self, ctx):
41 self.ctx = ctx
42
43 def get_fname(self):
44 return self.fname
45
46 def write_out(self, fname):
47 raise NotImplementedError
48
49 def pan(self, x, y, width, height):
50 """A surface actually represents just a ``window'' into
51 what we are drawing on. For instance, if we are scrolling through
52 a graph, then the surface represents the area in the GUI window,
53 not the entire graph (visible or not). So this method basically
54 moves the ``window's'' upper-left corner to (x, y), and resizes
55 the dimensions to (width, height)."""
56 self.virt_x = x
57 self.virt_y = y
58 self.width = width
59 self.height = height
60
61 def set_scale(self, scale):
62 """Sets the scale factor."""
63 self.scale = scale
64
65 def get_real_coor(self, x, y):
66 """Translates the coordinates (x, y)
67 in the ``theoretical'' plane to the true (x, y) coordinates on this surface
68 that we should draw to. Note that these might actually be outside the
69 bounds of the surface,
70 if we want something outside the surface's ``window''."""
71 return (x - self.virt_x * self.scale, y - self.virt_y * self.scale)
72
73 def get_virt_coor(self, x, y):
74 """Does the inverse of the last method."""
75 return (x + self.virt_x * self.scale, y + self.virt_y * self.scale)
76
77 def get_virt_coor_unscaled(self, x, y):
78 """Does the same, but removes the scale factor (i.e. behaves as if
79 the scale was 1.0 all along)."""
80 return (x / self.scale + self.virt_x, y / self.scale + self.virt_y)
81
82class SVGSurface(Surface):
83 def renew(self, width, height):
84 iwidth = int(math.ceil(width))
85 iheight = int(math.ceil(height))
86 self.surface = cairo.SVGSurface(self.fname, iwidth, iheight)
87 self.ctx = cairo.Context(self.surface)
88
89 def write_out(self, fname):
90 os.execl('cp', self.fname, fname)
91
92class ImageSurface(Surface):
93 def renew(self, width, height):
94 iwidth = int(math.ceil(width))
95 iheight = int(math.ceil(height))
96 self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, iwidth, iheight)
97 self.ctx = cairo.Context(self.surface)
98
99 def write_out(self, fname):
100 if self.surface is None:
101 raise ValueError('Don\'t own surface, can\'t write to to file')
102
103 self.surface.write_to_png(fname)
104
105class Pattern(object):
106 DEF_STRIPE_SIZE = 10
107 MAX_FADE_WIDTH = 250
108
109 def __init__(self, color_list, stripe_size=DEF_STRIPE_SIZE):
110 self.color_list = color_list
111 self.stripe_size = stripe_size
112
113 def render_on_canvas(self, canvas, x, y, width, height, fade=False):
114 fade_span = min(width, Pattern.MAX_FADE_WIDTH)
115
116 if len(self.color_list) == 1:
117 if fade:
118 canvas.fill_rect_fade(x, y, fade_span, height, (1.0, 1.0, 1.0), \
119 self.color_list[0])
120 else:
121 canvas.fill_rect(x, y, width, height, self.color_list[0])
122
123 if width > Pattern.MAX_FADE_WIDTH:
124 canvas.fill_rect(x + Pattern.MAX_FADE_WIDTH, y, width - Pattern.MAX_FADE_WIDTH,
125 height, self.color_list[0])
126 else:
127 n = 0
128 bottom = y + height
129 while y < bottom:
130 i = n % len(self.color_list)
131 if fade:
132 canvas.fill_rect_fade(x, y, fade_span, \
133 min(self.stripe_size, bottom - y), (1.0, 1.0, 1.0), self.color_list[i])
134 else:
135 canvas.fill_rect(x, y, width, min(self.stripe_size, bottom - y), self.color_list[i])
136
137 if width > Pattern.MAX_FADE_WIDTH:
138 canvas.fill_rect(x + Pattern.MAX_FADE_WIDTH, y, width - Pattern.MAX_FADE_WIDTH,
139 min(self.stripe_size, bottom - y), self.color_list[i])
140
141 y += self.stripe_size
142 n += 1
143
144class Canvas(object):
145 """This is a basic class that stores and draws on a Cairo surface,
146 using various primitives related to drawing a real-time graph (up-arrows,
147 down-arrows, bars, ...).
148
149 This is the lowest-level representation (aside perhaps from the Cairo
150 surface itself) of a real-time graph. It allows the user to draw
151 primitives at certain locations, but for the most part does not know
152 anything about real-time scheduling, just how to draw the basic parts
153 that make up a schedule graph. For that, see Graph or its descendants."""
154
155 BOTTOM_LAYER = 0
156 MIDDLE_LAYER = 1
157 TOP_LAYER = 2
158
159 LAYERS = (BOTTOM_LAYER, MIDDLE_LAYER, TOP_LAYER)
160
161 NULL_PATTERN = -1
162
163 SQRT3 = math.sqrt(3.0)
164
165 def __init__(self, width, height, item_clist, bar_plist, surface):
166 """Creates a new Canvas of dimensions (width, height). The
167 parameters ``item_plist'' and ``bar_plist'' each specify a list
168 of patterns to choose from when drawing the items on the y-axis
169 or filling in bars, respectively."""
170
171 self.surface = surface
172
173 self.width = int(math.ceil(width))
174 self.height = int(math.ceil(height))
175 self.item_clist = item_clist
176 self.bar_plist = bar_plist
177
178 self.selectable_regions = {}
179
180 self.scale = 1.0
181
182 # clears the canvas.
183 def clear(self):
184 raise NotImplementedError
185
186 def set_scale(self, scale):
187 self.scale = scale
188 self.surface.set_scale(scale)
189 for event in self.selectable_regions:
190 self.selectable_regions[event].set_scale(scale)
191
192 def scaled(self, *coors):
193 """Scales a series of coordinates."""
194 return [coor * self.scale for coor in coors]
195
196 def unscaled(self, *coors):
197 """Inverse of scale()."""
198 return [coor / self.scale for coor in coors]
199
200 def draw_rect(self, x, y, width, height, color, thickness, snap=True):
201 """Draws a rectangle somewhere (border only)."""
202 raise NotImplementedError
203
204 def fill_rect(self, x, y, width, height, color, snap=True):
205 """Draws a filled rectangle somewhere. ``color'' is a 3-tuple."""
206 raise NotImplementedError
207
208 def fill_rect_fade(self, x, y, width, height, lcolor, rcolor, snap=True):
209 """Draws a rectangle somewhere, filled in with the fade."""
210 raise NotImplementedError
211
212 def draw_line(self, p0, p1, color, thickness, snap=True):
213 """Draws a line from p0 to p1 with a certain color and thickness."""
214 raise NotImplementedError
215
216 def draw_polyline(self, coor_list, color, thickness, snap=True):
217 """Draws a polyline, where coor_list = [(x_0, y_0), (x_1, y_1), ... (x_m, y_m)]
218 specifies a polyline from (x_0, y_0) to (x_1, y_1), etc."""
219 raise NotImplementedError
220
221 def fill_polyline(self, coor_list, color, thickness, snap=True):
222 """Draws a polyline (probably a polygon) and fills it."""
223 raise NotImplementedError
224
225 def draw_label(self, text, x, y, fopts=GraphFormat.DEF_FOPTS_LABEL,
226 halign=AlignMode.LEFT, valign=AlignMode.BOTTOM, snap=True):
227 """Draws text at a position with a certain alignment."""
228 raise NotImplementedError
229
230 def draw_label_with_sscripts(self, text, supscript, subscript, x, y, \
231 textfopts=GraphFormat.DEF_FOPTS_LABEL,
232 sscriptfopts=GraphFormat.DEF_FOPTS_LABEL_SSCRIPT, \
233 halign=AlignMode.LEFT, valign=AlignMode.BOTTOM, snap=True):
234 """Draws text at a position with a certain alignment, along with optionally a superscript and
235 subscript (which are None if either is not used.)"""
236 raise NotImplementedError
237
238 def draw_y_axis(self, x, y, height):
239 """Draws the y-axis, starting from the bottom at the point x, y."""
240 self.surface.ctx.set_source_rgb(0.0, 0.0, 0.0)
241
242 self.draw_line((x, y), (x, y - height), (0.0, 0.0, 0.0), GraphFormat.AXIS_THICKNESS)
243
244 def draw_y_axis_labels(self, x, y, height, item_list, item_size, fopts=None):
245 """Draws the item labels on the y-axis. ``item_list'' is the list
246 of strings to print, while item_size gives the vertical amount of
247 space that each item shall take up, in pixels."""
248 if fopts is None:
249 fopts = GraphFormat.DEF_FOPTS_ITEM
250
251 x -= GraphFormat.Y_AXIS_ITEM_GAP
252 y -= height - item_size / 2.0
253
254 orig_color = fopts.color
255 for ctr, item in enumerate(item_list):
256 fopts.color = self.get_item_color(ctr)
257 self.draw_label(item, x, y, fopts, AlignMode.RIGHT, AlignMode.CENTER)
258 y += item_size
259
260 fopts.color = orig_color
261
262 def draw_x_axis(self, x, y, start_tick, end_tick, maj_sep, min_per_maj):
263 """Draws the x-axis, including all the major and minor ticks (but not the labels).
264 ``num_maj'' gives the number of major ticks, ``maj_sep'' the number of pixels between
265 major ticks, and ``min_per_maj'' the number of minor ticks between two major ticks
266 (including the first major tick)"""
267 self.draw_line((x, y), (x + GraphFormat.X_AXIS_MEASURE_OFS, y),
268 (0.0, 0.0, 0.0), GraphFormat.AXIS_THICKNESS)
269 x += GraphFormat.X_AXIS_MEASURE_OFS + start_tick * maj_sep
270
271 for i in range(start_tick, end_tick + 1):
272 self.draw_line((x, y), (x, y + GraphFormat.MAJ_TICK_SIZE),
273 (0.0, 0.0, 0.0), GraphFormat.AXIS_THICKNESS)
274
275 if (i < end_tick):
276 for j in range(0, min_per_maj):
277 self.draw_line((x, y), (x + maj_sep / min_per_maj, y),
278 (0.0, 0.0, 0.0), GraphFormat.AXIS_THICKNESS)
279
280 x += 1.0 * maj_sep / min_per_maj
281 if j < min_per_maj - 1:
282 self.draw_line((x, y), (x, y + GraphFormat.MIN_TICK_SIZE),
283 (0.0, 0.0, 0.0), GraphFormat.AXIS_THICKNESS)
284
285 def draw_x_axis_labels(self, x, y, start_tick, end_tick, maj_sep, min_per_maj, start=0, incr=1, show_min=False, \
286 majfopts=GraphFormat.DEF_FOPTS_MAJ, minfopts=GraphFormat.DEF_FOPTS_MIN):
287 """Draws the labels for the x-axis. (x, y) should give the origin.
288 how far down you want the text. ``incr'' gives the increment per major
289 tick. ``start'' gives the value of the first tick. ``show_min'' specifies
290 whether to draw labels at minor ticks."""
291
292 x += GraphFormat.X_AXIS_MEASURE_OFS + start_tick * maj_sep
293 y += GraphFormat.X_AXIS_LABEL_GAP + GraphFormat.MAJ_TICK_SIZE
294
295 minincr = incr / (min_per_maj * 1.0)
296
297 cur = start * 1.0
298
299 for i in range(start_tick, end_tick + 1):
300 text = util.format_float(cur, 2)
301 self.draw_label(text, x, y, majfopts, AlignMode.CENTER, AlignMode.TOP)
302
303 if (i < end_tick):
304 if show_min:
305 for j in range(0, min_per_maj):
306 x += 1.0 * maj_sep / min_per_maj
307 cur += minincr
308 text = util.format_float(cur, 2)
309
310 if j < min_per_maj - 1:
311 self.draw_label(text, x, y, minfopts, AlignMode.CENTER, AlignMode.TOP)
312 else:
313 x += maj_sep
314 cur += incr
315
316 def draw_grid(self, x, y, height, start_tick, end_tick, start_item, end_item, maj_sep, item_size, \
317 min_per_maj=None, show_min=False):
318 """Draws a grid dividing along the item boundaries and the major ticks.
319 (x, y) gives the origin. ``show_min'' specifies whether to draw vertical grid lines at minor ticks.
320 ``start_tick'' and ``end_tick'' give the major ticks to start and end at for drawing vertical lines.
321 ``start_item'' and ``end_item'' give the item boundaries to start and end drawing horizontal lines."""
322 if start_tick > end_tick or start_item > end_item:
323 return
324
325 line_width = (end_tick - start_tick) * maj_sep
326 line_height = (end_item - start_item) * item_size
327
328 origin = (x, y)
329
330 # draw horizontal lines first
331 x = origin[0] + GraphFormat.X_AXIS_MEASURE_OFS + start_tick * maj_sep
332 y = origin[1] - height + start_item * item_size
333 for i in range(start_item, end_item + 1):
334 self.draw_line((x, y), (x + line_width, y), GraphFormat.GRID_COLOR, GraphFormat.GRID_THICKNESS)
335 y += item_size
336
337 x = origin[0] + GraphFormat.X_AXIS_MEASURE_OFS + start_tick * maj_sep
338 y = origin[1] - height + start_item * item_size
339
340 if show_min:
341 for i in range(0, (end_tick - start_tick) * min_per_maj + 1):
342 self.draw_line((x, y), (x, y + line_height), GraphFormat.GRID_COLOR, GraphFormat.GRID_THICKNESS)
343 x += maj_sep * 1.0 / min_per_maj
344 else:
345 for i in range(start_tick, end_tick + 1):
346 self.draw_line((x, y), (x, y + line_height), GraphFormat.GRID_COLOR, GraphFormat.GRID_THICKNESS)
347 x += maj_sep
348
349 def draw_bar(self, x, y, width, height, n, clip_side, selected):
350 """Draws a bar with a certain set of dimensions, using pattern ``n'' from the
351 bar pattern list."""
352
353 color, thickness = {False : (GraphFormat.BORDER_COLOR, GraphFormat.BORDER_THICKNESS),
354 True : (GraphFormat.HIGHLIGHT_COLOR, GraphFormat.BORDER_THICKNESS * 2.0)}[selected]
355
356 # use a pattern to be pretty
357 self.get_bar_pattern(n).render_on_canvas(self, x, y, width, height, True)
358
359 self.draw_rect(x, y, width, height, color, thickness, clip_side)
360
361 def add_sel_bar(self, x, y, width, height, event):
362 self.add_sel_region(SelectableRegion(x, y, width, height, event))
363
364 def draw_mini_bar(self, x, y, width, height, n, clip_side, selected):
365 """Like the above, except it draws a miniature version. This is usually used for
366 secondary purposes (i.e. to show jobs that _should_ have been running at a certain time).
367
368 Of course we don't enforce the fact that this is mini, since the user can pass in width
369 and height (but the mini bars do look slightly different: namely the borders are a different
370 color)"""
371
372 color, thickness = {False : (GraphFormat.LITE_BORDER_COLOR, GraphFormat.BORDER_THICKNESS),
373 True : (GraphFormat.HIGHLIGHT_COLOR, GraphFormat.BORDER_THICKNESS * 1.5)}[selected]
374
375 self.get_bar_pattern(n).render_on_canvas(self, x, y, width, height, True)
376
377 self.draw_rect(x, y, width, height, color, thickness, clip_side)
378
379 def add_sel_mini_bar(self, x, y, width, height, event):
380 self.add_sel_region(SelectableRegion(x, y, width, height, event))
381
382 def draw_completion_marker(self, x, y, height, selected):
383 """Draws the symbol that represents a job completion, using a certain height."""
384
385 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
386 self.draw_line((x - height * GraphFormat.TEE_FACTOR / 2.0, y),
387 (x + height * GraphFormat.TEE_FACTOR / 2.0, y),
388 color, GraphFormat.BORDER_THICKNESS)
389 self.draw_line((x, y), (x, y + height), color, GraphFormat.BORDER_THICKNESS)
390
391 def add_sel_completion_marker(self, x, y, height, event):
392 self.add_sel_region(SelectableRegion(x - height * GraphFormat.TEE_FACTOR / 2.0, y,
393 height * GraphFormat.TEE_FACTOR, height, event))
394
395 def draw_release_arrow_big(self, x, y, height, selected):
396 """Draws a release arrow of a certain height: (x, y) should give the top
397 (northernmost point) of the arrow. The height includes the arrowhead."""
398 big_arrowhead_height = GraphFormat.BIG_ARROWHEAD_FACTOR * height
399
400 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
401 colors = [(1.0, 1.0, 1.0), color]
402 draw_funcs = [self.__class__.fill_polyline, self.__class__.draw_polyline]
403 for i in range(0, 2):
404 color = colors[i]
405 draw_func = draw_funcs[i]
406
407 draw_func(self, [(x, y), (x - big_arrowhead_height / Canvas.SQRT3, y + big_arrowhead_height), \
408 (x + big_arrowhead_height / Canvas.SQRT3, y + big_arrowhead_height), (x, y)], \
409 color, GraphFormat.BORDER_THICKNESS)
410
411 self.draw_line((x, y + big_arrowhead_height), (x, y + height), color, GraphFormat.BORDER_THICKNESS)
412
413 def add_sel_release_arrow_big(self, x, y, height, event):
414 self.add_sel_arrow_big(x, y, height, event)
415
416 def draw_deadline_arrow_big(self, x, y, height, selected):
417 """Draws a release arrow: x, y should give the top (northernmost
418 point) of the arrow. The height includes the arrowhead."""
419 big_arrowhead_height = GraphFormat.BIG_ARROWHEAD_FACTOR * height
420
421 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
422 colors = [(1.0, 1.0, 1.0), color]
423 draw_funcs = [self.__class__.fill_polyline, self.__class__.draw_polyline]
424 for i in range(0, 2):
425 color = colors[i]
426 draw_func = draw_funcs[i]
427
428 draw_func(self, [(x, y + height), (x - big_arrowhead_height / Canvas.SQRT3, \
429 y + height - big_arrowhead_height), \
430 (x + big_arrowhead_height / Canvas.SQRT3, \
431 y + height - big_arrowhead_height), \
432 (x, y + height)], color, GraphFormat.BORDER_THICKNESS)
433
434 self.draw_line((x, y), (x, y + height - big_arrowhead_height),
435 color, GraphFormat.BORDER_THICKNESS)
436
437 def add_sel_deadline_arrow_big(self, x, y, height, event):
438 self.add_sel_arrow_big(x, y, height, event)
439
440 def add_sel_arrow_big(self, x, y, height, event):
441 big_arrowhead_height = GraphFormat.BIG_ARROWHEAD_FACTOR * height
442
443 self.add_sel_region(SelectableRegion(x - big_arrowhead_height / Canvas.SQRT3,
444 y, 2.0 * big_arrowhead_height / Canvas.SQRT3, height, event))
445
446 def draw_release_arrow_small(self, x, y, height, selected):
447 """Draws a small release arrow (most likely coming off the x-axis, although
448 this method doesn't enforce this): x, y should give the top of the arrow"""
449 small_arrowhead_height = GraphFormat.SMALL_ARROWHEAD_FACTOR * height
450
451 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
452
453 self.draw_line((x, y), (x - small_arrowhead_height, y + small_arrowhead_height), \
454 color, GraphFormat.BORDER_THICKNESS)
455 self.draw_line((x, y), (x + small_arrowhead_height, y + small_arrowhead_height), \
456 color, GraphFormat.BORDER_THICKNESS)
457 self.draw_line((x, y), (x, y + height), color, GraphFormat.BORDER_THICKNESS)
458
459 def add_sel_release_arrow_small(self, x, y, height, event):
460 self.add_sel_arrow_small(x, y, height, event)
461
462 def draw_deadline_arrow_small(self, x, y, height, selected):
463 """Draws a small deadline arrow (most likely coming off the x-axis, although
464 this method doesn't enforce this): x, y should give the top of the arrow"""
465 small_arrowhead_height = GraphFormat.SMALL_ARROWHEAD_FACTOR * height
466
467 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
468
469 self.draw_line((x, y), (x, y + height), color, GraphFormat.BORDER_THICKNESS)
470 self.draw_line((x - small_arrowhead_height, y + height - small_arrowhead_height), \
471 (x, y + height), color, GraphFormat.BORDER_THICKNESS)
472 self.draw_line((x + small_arrowhead_height, y + height - small_arrowhead_height), \
473 (x, y + height), color, GraphFormat.BORDER_THICKNESS)
474
475 def add_sel_deadline_arrow_small(self, x, y, height, event):
476 self.add_sel_arrow_small(x, y, height, event)
477
478 def add_sel_arrow_small(self, x, y, height, event):
479 small_arrowhead_height = GraphFormat.SMALL_ARROWHEAD_FACTOR * height
480
481 self.add_sel_region(SelectableRegion(x - small_arrowhead_height, y,
482 small_arrowhead_height * 2.0, height, event))
483
484 def draw_suspend_triangle(self, x, y, height, selected):
485 """Draws the triangle that marks a suspension. (x, y) gives the topmost (northernmost) point
486 of the symbol."""
487
488 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
489 colors = [(0.0, 0.0, 0.0), color]
490
491 draw_funcs = [self.__class__.fill_polyline, self.__class__.draw_polyline]
492 for i in range(0, 2):
493 color = colors[i]
494 draw_func = draw_funcs[i]
495 draw_func(self, [(x, y), (x + height / 2.0, y + height / 2.0), (x, y + height), (x, y)], \
496 color, GraphFormat.BORDER_THICKNESS)
497
498 def add_sel_suspend_triangle(self, x, y, height, event):
499 self.add_sel_region(SelectableRegion(x, y, height / 2.0, height, event))
500
501 def draw_resume_triangle(self, x, y, height, selected):
502 """Draws the triangle that marks a resumption. (x, y) gives the topmost (northernmost) point
503 of the symbol."""
504
505 color = {False : GraphFormat.BORDER_COLOR, True : GraphFormat.HIGHLIGHT_COLOR}[selected]
506 colors = [(1.0, 1.0, 1.0), color]
507
508 draw_funcs = [self.__class__.fill_polyline, self.__class__.draw_polyline]
509 for i in range(0, 2):
510 color = colors[i]
511 draw_func = draw_funcs[i]
512 draw_func(self, [(x, y), (x - height / 2.0, y + height / 2.0), (x, y + height), (x, y)], \
513 color, GraphFormat.BORDER_THICKNESS)
514
515 def add_sel_resume_triangle(self, x, y, height, event):
516 self.add_sel_region(SelectableRegion(x - height / 2.0, y, height / 2.0, height, event))
517
518 def clear_selectable_regions(self):
519 self.selectable_regions = {}
520
521 #def clear_selectable_regions(self, real_x, real_y, width, height):
522 # x, y = self.surface.get_virt_coor(real_x, real_y)
523 # for event in self.selectable_regions.keys():
524 # if self.selectable_regions[event].intersects(x, y, width, height):
525 # del self.selectable_regions[event]
526
527 def add_sel_region(self, region):
528 region.set_scale(self.scale)
529 self.selectable_regions[region.get_event()] = region
530
531 def get_sel_region(self, event):
532 return self.selectable_regions[event]
533
534 def has_sel_region(self, event):
535 return event in self.selectable_regions
536
537 def get_selected_regions(self, real_x, real_y, width, height):
538 x, y = self.surface.get_virt_coor(real_x, real_y)
539
540 selected = {}
541 for event in self.selectable_regions:
542 region = self.selectable_regions[event]
543 if region.intersects(x, y, width, height):
544 selected[event] = region
545
546 return selected
547
548 def whiteout(self):
549 """Overwrites the surface completely white, but technically doesn't delete anything"""
550 # Make sure we don't scale here (we want to literally white out just this region)
551
552 x, y = self.surface.get_virt_coor_unscaled(0, 0)
553 width, height = self.unscaled(self.surface.width, self.surface.height)
554
555 self.fill_rect(x, y, width, height, (1.0, 1.0, 1.0), False)
556
557 def get_item_color(self, n):
558 """Gets the nth color in the item color list, which are the colors used to draw the items
559 on the y-axis. Note that there are conceptually infinitely
560 many patterns because the patterns repeat -- that is, we just mod out by the size of the pattern
561 list when indexing."""
562 return self.item_clist[n % len(self.item_clist)]
563
564 def get_bar_pattern(self, n):
565 """Gets the nth pattern in the bar pattern list, which is a list of surfaces that are used to
566 fill in the bars. Note that there are conceptually infinitely
567 many patterns because the patterns repeat -- that is, we just mod out by the size of the pattern
568 list when indexing."""
569 if n < 0:
570 return self.bar_plist[-1]
571 return self.bar_plist[n % (len(self.bar_plist) - 1)]
572
573class CairoCanvas(Canvas):
574 """This is a basic class that stores and draws on a Cairo surface,
575 using various primitives related to drawing a real-time graph (up-arrows,
576 down-arrows, bars, ...).
577
578 This is the lowest-level non-abstract representation
579 (aside perhaps from the Cairo surface itself) of a real-time graph.
580 It allows the user to draw primitives at certain locations, but for
581 the most part does not know anything about real-time scheduling,
582 just how to draw the basic parts that make up a schedule graph.
583 For that, see Graph or its descendants."""
584
585 #def __init__(self, fname, width, height, item_clist, bar_plist, surface):
586 # """Creates a new Canvas of dimensions (width, height). The
587 # parameters ``item_plist'' and ``bar_plist'' each specify a list
588 # of patterns to choose from when drawing the items on the y-axis
589 # or filling in bars, respectively."""
590
591 # super(CairoCanvas, self).__init__(fname, width, height, item_clist, bar_plist, surface)
592
593 #def clear(self):
594 # self.surface = self.SurfaceType(self.width, self.height, self.fname)
595 # self.whiteout()
596
597 def get_surface(self):
598 """Gets the Surface that we are drawing on in its current state."""
599 return self.surface
600
601 def _rect_common(self, x, y, width, height, color, thickness, clip_side=None, do_snap=True):
602 EXTRA_FACTOR = 2.0
603
604 x, y, width, height = self.scaled(x, y, width, height)
605 x, y = self.surface.get_real_coor(x, y)
606 max_width = self.surface.width + EXTRA_FACTOR * thickness
607 max_height = self.surface.height + EXTRA_FACTOR * thickness
608
609 # if dimensions are really large this can cause Cairo problems --
610 # so clip it to the size of the surface, which is the only part we see anyway
611 if x < 0:
612 width += x
613 x = 0
614 if y < 0:
615 height += y
616 y = 0
617 if width > max_width:
618 width = max_width
619 if height > max_height:
620 height = max_height
621
622 if do_snap:
623 x = snap(x)
624 y = snap(y)
625
626 if clip_side == AlignMode.LEFT:
627 self.surface.ctx.move_to(x, y)
628 self.surface.ctx.line_to(x + width, y)
629 self.surface.ctx.line_to(x + width, y + height)
630 self.surface.ctx.line_to(x, y + height)
631 elif clip_side == AlignMode.RIGHT:
632 self.surface.ctx.move_to(x + width, y)
633 self.surface.ctx.line_to(x, y)
634 self.surface.ctx.line_to(x, y + height)
635 self.surface.ctx.line_to(x + width, y + height)
636 else:
637 # don't clip one edge of the rectangle -- just draw a Cairo rectangle
638 self.surface.ctx.rectangle(x, y, width, height)
639
640 self.surface.ctx.set_line_width(thickness * self.scale)
641 self.surface.ctx.set_source_rgb(color[0], color[1], color[2])
642
643 def draw_rect(self, x, y, width, height, color, thickness, clip_side=None, do_snap=True):
644 self._rect_common(x, y, width, height, color, thickness, clip_side, do_snap)
645 self.surface.ctx.stroke()
646
647 def fill_rect(self, x, y, width, height, color, do_snap=True):
648 self._rect_common(x, y, width, height, color, 1, do_snap)
649 self.surface.ctx.fill()
650
651 def fill_rect_fade(self, x, y, width, height, lcolor, rcolor, do_snap=True):
652 """Draws a rectangle somewhere, filled in with the fade."""
653 x, y, width, height = self.scaled(x, y, width, height)
654 x, y = self.surface.get_real_coor(x, y)
655
656 if do_snap:
657 linear = cairo.LinearGradient(snap(x), snap(y), \
658 snap(x + width), snap(y + height))
659 else:
660 linear = cairo.LinearGradient(x, y, \
661 x + width, y + height)
662 linear.add_color_stop_rgb(0.0, lcolor[0], lcolor[1], lcolor[2])
663 linear.add_color_stop_rgb(1.0, rcolor[0], rcolor[1], rcolor[2])
664 self.surface.ctx.set_source(linear)
665 if do_snap:
666 self.surface.ctx.rectangle(snap(x), snap(y), width, height)
667 else:
668 self.surface.ctx.rectangle(x, y, width, height)
669 self.surface.ctx.fill()
670
671 def draw_line(self, p0, p1, color, thickness, do_snap=True):
672 """Draws a line from p0 to p1 with a certain color and thickness."""
673 p0 = self.scaled(p0[0], p0[1])
674 p0 = self.surface.get_real_coor(p0[0], p0[1])
675 p1 = self.scaled(p1[0], p1[1])
676 p1 = self.surface.get_real_coor(p1[0], p1[1])
677 if do_snap:
678 p0 = (snap(p0[0]), snap(p0[1]))
679 p1 = (snap(p1[0]), snap(p1[1]))
680
681 self.surface.ctx.move_to(p0[0], p0[1])
682 self.surface.ctx.line_to(p1[0], p1[1])
683 self.surface.ctx.set_source_rgb(color[0], color[1], color[2])
684 self.surface.ctx.set_line_width(thickness * self.scale)
685 self.surface.ctx.stroke()
686
687 def _polyline_common(self, coor_list, color, thickness, do_snap=True):
688 scaled_coor_list = [self.scaled(coor[0], coor[1]) for coor in coor_list]
689 real_coor_list = [self.surface.get_real_coor(coor[0], coor[1]) for coor in scaled_coor_list]
690
691 self.surface.ctx.move_to(real_coor_list[0][0], real_coor_list[0][1])
692 if do_snap:
693 for i in range(0, len(real_coor_list)):
694 real_coor_list[i] = (snap(real_coor_list[i][0]), snap(real_coor_list[i][1]))
695
696 for coor in real_coor_list[1:]:
697 self.surface.ctx.line_to(coor[0], coor[1])
698
699 self.surface.ctx.set_line_width(thickness * self.scale)
700 self.surface.ctx.set_source_rgb(color[0], color[1], color[2])
701
702 def draw_polyline(self, coor_list, color, thickness, do_snap=True):
703 self._polyline_common(coor_list, color, thickness, do_snap)
704 self.surface.ctx.stroke()
705
706 def fill_polyline(self, coor_list, color, thickness, do_snap=True):
707 self._polyline_common(coor_list, color, thickness, do_snap)
708 self.surface.ctx.fill()
709
710 def _draw_label_common(self, text, x, y, fopts, x_bearing_factor, \
711 f_descent_factor, width_factor, f_height_factor, do_snap=True):
712 """Helper function for drawing a label with some alignment. Instead of taking in an alignment,
713 it takes in the scale factor for the font extent parameters, which give the raw data of how much to adjust
714 the x and y parameters. Only should be used internally."""
715 x, y = self.scaled(x, y)
716 x, y = self.surface.get_real_coor(x, y)
717
718 self.surface.ctx.set_source_rgb(0.0, 0.0, 0.0)
719
720 self.surface.ctx.select_font_face(fopts.name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
721 self.surface.ctx.set_font_size(fopts.size * self.scale)
722
723 fe = self.surface.ctx.font_extents()
724 f_ascent, f_descent, f_height = fe[:3]
725
726 te = self.surface.ctx.text_extents(text)
727 x_bearing, y_bearing, width, height = te[:4]
728
729 actual_x = x - x_bearing * x_bearing_factor - width * width_factor
730 actual_y = y - f_descent * f_descent_factor + f_height * f_height_factor
731
732 self.surface.ctx.set_source_rgb(fopts.color[0], fopts.color[1], fopts.color[2])
733
734 if do_snap:
735 self.surface.ctx.move_to(snap(actual_x), snap(actual_y))
736 else:
737 self.surface.ctx.move_to(actual_x, actual_y)
738
739 self.surface.ctx.show_text(text)
740
741 def draw_label(self, text, x, y, fopts=GraphFormat.DEF_FOPTS_LABEL, halign=AlignMode.LEFT, valign=AlignMode.BOTTOM, do_snap=True):
742 """Draws a label with the given parameters, with the given horizontal and vertical justification. One can override
743 the color from ``fopts'' by passing something in to ``pattern'', which overrides the color with an arbitrary
744 pattern."""
745 x_bearing_factor, f_descent_factor, width_factor, f_height_factor = 0.0, 0.0, 0.0, 0.0
746 halign_factors = {AlignMode.LEFT : (0.0, 0.0), AlignMode.CENTER : (1.0, 0.5), AlignMode.RIGHT : (1.0, 1.0)}
747 if halign not in halign_factors:
748 raise ValueError('Invalid alignment value')
749 x_bearing_factor, width_factor = halign_factors[halign]
750
751 valign_factors = {AlignMode.BOTTOM : (0.0, 0.0), AlignMode.CENTER : (1.0, 0.5), AlignMode.TOP : (1.0, 1.0)}
752 if valign not in valign_factors:
753 raise ValueError('Invalid alignment value')
754 f_descent_factor, f_height_factor = valign_factors[valign]
755
756 self._draw_label_common(text, x, y, fopts, x_bearing_factor, \
757 f_descent_factor, width_factor, f_height_factor, do_snap)
758
759 def draw_label_with_sscripts(self, text, supscript, subscript, x, y, \
760 textfopts=GraphFormat.DEF_FOPTS_LABEL, sscriptfopts=GraphFormat.DEF_FOPTS_LABEL_SSCRIPT, \
761 halign=AlignMode.LEFT, valign=AlignMode.BOTTOM, do_snap=True):
762 """Draws a label, but also optionally allows a superscript and subscript to be rendered."""
763 self.draw_label(text, x, y, textfopts, halign, valign)
764
765 self.surface.ctx.set_source_rgb(0.0, 0.0, 0.0)
766 self.surface.ctx.select_font_face(textfopts.name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
767 self.surface.ctx.set_font_size(textfopts.size)
768 te = self.surface.ctx.text_extents(text)
769 fe = self.surface.ctx.font_extents()
770 if supscript is not None:
771 f_height = fe[2]
772 x_advance = te[4]
773 xtmp = x + x_advance
774 ytmp = y
775 ytmp = y - f_height / 4.0
776 self.draw_label(supscript, xtmp, ytmp, sscriptfopts, halign, valign, do_snap)
777 if subscript is not None:
778 f_height = fe[2]
779 x_advance = te[4]
780 xtmp = x + x_advance
781 ytmp = y
782 ytmp = y + f_height / 4.0
783 self.draw_label(subscript, xtmp, ytmp, sscriptfopts, halign, valign, do_snap)
784
785# represents a selectable region of the graph
786class SelectableRegion(object):
787 def __init__(self, x, y, width, height, event):
788 self.x = x
789 self.y = y
790 self.width = width
791 self.height = height
792 self.event = event
793 self.scale = 1.0
794
795 def get_dimensions(self):
796 return (self.x, self.y, self.width, self.height)
797
798 def get_event(self):
799 return self.event
800
801 def set_scale(self, scale):
802 self.scale = scale
803
804 def intersects(self, x, y, width, height):
805 return x <= (self.x + self.width) * self.scale \
806 and x + width >= self.x * self.scale \
807 and y <= (self.y + self.height) * self.scale \
808 and y + height >= self.y * self.scale
809