diff options
author | Gary Bressler <garybressler@nc.rr.com> | 2010-04-06 12:45:04 -0400 |
---|---|---|
committer | Gary Bressler <garybressler@nc.rr.com> | 2010-04-06 12:45:04 -0400 |
commit | c7e3aaebdba7bf880534abd91a383b5543cf0be4 (patch) | |
tree | 048977efdaaa3d60e93c3d21ba29c46a0bfe71c3 /unit_trace/viz/canvas.py | |
parent | 7fdb4dbbbca577efbeec47cd1364eb319346a0cc (diff) |
Making sure everything committed
Diffstat (limited to 'unit_trace/viz/canvas.py')
-rw-r--r-- | unit_trace/viz/canvas.py | 809 |
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 | ||
4 | this file is quite low-level, in that its objects are mostly restricted to | ||
5 | dealing with drawing the components of a real-time graph given coordinates | ||
6 | rather than having an abstract knowledge of the graph's measurements or | ||
7 | any information about events.""" | ||
8 | |||
9 | import math | ||
10 | import cairo | ||
11 | import os | ||
12 | import copy | ||
13 | |||
14 | import util | ||
15 | from format import * | ||
16 | |||
17 | def 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 | |||
26 | class 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 | |||
82 | class 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 | |||
92 | class 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 | |||
105 | class 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 | |||
144 | class 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 | |||
573 | class 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 | ||
786 | class 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 | |||