utils/grid.py
author Josh Pelkey <jpelkey@gatech.edu>
Wed, 11 Aug 2010 11:37:37 -0400
changeset 6553 fb5ad9c7755a
parent 136 4faf1726e8ff
permissions -rw-r--r--
update release notes and fix doxygen warnings
     1 #!/usr/bin/env python
     2 ## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
     3 
     4 import cairo
     5 import sys
     6 import re
     7 import gtk
     8 
     9 
    10 
    11 class DataRange:
    12     def __init__(self, start = 0, end = 0, value = ''):
    13         self.start = start
    14         self.end = end
    15         self.value = value
    16 class EventString:
    17     def __init__(self, at = 0, value = ''):
    18         self.at = at
    19         self.value = value
    20 class EventFloat:
    21     def __init__(self, at = 0, value = 0.0):
    22         self.at = at
    23         self.value = value
    24 class EventInt:
    25     def __init__(self, at = 0, value = 0.0):
    26         self.at = at
    27         self.value = value
    28 def ranges_cmp(a, b):
    29     diff = a.start - b.start
    30     if diff < 0:
    31         return -1
    32     elif diff > 0:
    33         return +1
    34     else:
    35         return 0
    36 def events_cmp(a, b):
    37     diff = a.at - b.at
    38     if diff < 0:
    39         return -1
    40     elif diff > 0:
    41         return +1
    42     else:
    43         return 0
    44 class TimelineDataRange:
    45     def __init__(self, name = ''):
    46         self.name = name
    47         self.ranges = []
    48         return
    49     def __search(self, key):
    50         l = 0
    51         u = len(self.ranges)-1
    52         while l <= u:
    53             i = int((l + u) / 2)
    54             if key >= self.ranges[i].start and key <= self.ranges[i].end:
    55                 return i
    56             elif key < self.ranges[i].start:
    57                 u = i - 1
    58             else:
    59                 # key > self.ranges[i].end
    60                 l = i + 1
    61         return - 1
    62     def add_range(self, range):
    63         self.ranges.append(range)
    64     def get_all(self):
    65         return self.ranges
    66     def get_ranges(self, start, end):
    67         s = self.__search(start)
    68         e = self.__search(end)
    69         if s == -1 and e == -1:
    70             return []
    71         elif s == -1:
    72             return self.ranges[0:e + 1]
    73         elif e == -1:
    74             return self.ranges[s:len(self.ranges)]
    75         else:
    76             return self.ranges[s:e + 1]
    77     def get_ranges_bounds(self, start, end):
    78         s = self.__search(start)
    79         e = self.__search(end)
    80         if s == -1 and e == -1:
    81             return(0, 0)
    82         elif s == -1:
    83             return(0, e + 1)
    84         elif e == -1:
    85             return(s, len(self.ranges))
    86         else:
    87             return(s, e + 1)
    88     def sort(self):
    89         self.ranges.sort(ranges_cmp)
    90     def get_bounds(self):
    91         if len(self.ranges) > 0:
    92             lo = self.ranges[0].start
    93             hi = self.ranges[len(self.ranges)-1].end
    94             return(lo, hi)
    95         else:
    96             return(0, 0)
    97 class TimelineEvent:
    98     def __init__(self, name = ''):
    99         self.name = name
   100         self.events = []
   101     def __search(self, key):
   102         l = 0
   103         u = len(self.events)-1
   104         while l <= u:
   105             i = int((l + u) / 2)
   106             if key == self.events[i].at:
   107                 return i
   108             elif key < self.events[i].at:
   109                 u = i - 1
   110             else:
   111                 # key > self.events[i].at
   112                 l = i + 1
   113         return l
   114     def add_event(self, event):
   115         self.events.append(event)
   116     def get_events(self, start, end):
   117         s = self.__search(start)
   118         e = self.__search(end)
   119         return self.events[s:e + 1]
   120     def get_events_bounds(self, start, end):
   121         s = self.__search(start)
   122         e = self.__search(end)
   123         return(s, e + 1)
   124     def sort(self):
   125         self.events.sort(events_cmp)
   126     def get_bounds(self):
   127         if len(self.events) > 0:
   128             lo = self.events[0].at
   129             hi = self.events[-1].at
   130             return(lo, hi)
   131         else:
   132             return(0, 0)
   133 
   134 class Timeline:
   135     def __init__(self, name = ''):
   136         self.ranges = []
   137         self.event_str = []
   138         self.event_int = []
   139         self.name = name
   140     def get_range(self, name):
   141         for range in self.ranges:
   142             if range.name == name:
   143                 return range
   144         timeline = TimelineDataRange(name)
   145         self.ranges.append(timeline)
   146         return timeline
   147     def get_event_str(self, name):
   148         for event_str in self.event_str:
   149             if event_str.name == name:
   150                 return event_str
   151         timeline = TimelineEvent(name)
   152         self.event_str.append(timeline)
   153         return timeline
   154     def get_event_int(self, name):
   155         for event_int in self.event_int:
   156             if event_int.name == name:
   157                 return event_int
   158         timeline = TimelineEvent(name)
   159         self.event_int.append(timeline)
   160         return timeline
   161     def get_ranges(self):
   162         return self.ranges
   163     def get_events_str(self):
   164         return self.event_str
   165     def get_events_int(self):
   166         return self.event_int
   167     def sort(self):
   168         for range in self.ranges:
   169             range.sort()
   170         for event in self.event_int:
   171             event.sort()
   172         for event in self.event_str:
   173             event.sort()
   174     def get_bounds(self):
   175         lo = 0
   176         hi = 0
   177         for range in self.ranges:
   178             (range_lo, range_hi) = range.get_bounds()
   179             if range_lo < lo:
   180                 lo = range_lo
   181             if range_hi > hi:
   182                 hi = range_hi
   183         for event_str in self.event_str:
   184             (ev_lo, ev_hi) = event_str.get_bounds()
   185             if ev_lo < lo:
   186                 lo = ev_lo
   187             if ev_hi > hi:
   188                 hi = ev_hi
   189         for event_int in self.event_int:
   190             (ev_lo, ev_hi) = event_int.get_bounds()
   191             if ev_lo < lo:
   192                 lo = ev_lo
   193             if ev_hi > hi:
   194                 hi = ev_hi
   195         return(lo, hi)
   196 class Timelines:
   197     def __init__(self):
   198         self.timelines = []
   199     def get(self, name):
   200         for timeline in self.timelines:
   201             if timeline.name == name:
   202                 return timeline
   203         timeline = Timeline(name)
   204         self.timelines.append(timeline)
   205         return timeline
   206     def get_all(self):
   207         return self.timelines
   208     def sort(self):
   209         for timeline in self.timelines:
   210             timeline.sort()
   211     def get_bounds(self):
   212         lo = 0
   213         hi = 0
   214         for timeline in self.timelines:
   215             (t_lo, t_hi) = timeline.get_bounds()
   216             if t_lo < lo:
   217                 lo = t_lo
   218             if t_hi > hi:
   219                 hi = t_hi
   220         return(lo, hi)
   221     def get_all_range_values(self):
   222         range_values = {}
   223         for timeline in self.timelines:
   224             for ranges in timeline.get_ranges():
   225                 for ran in ranges.get_all():
   226                     range_values[ran.value] = 1
   227         return range_values.keys()
   228 class Color:
   229     def __init__(self, r = 0.0, g = 0.0, b = 0.0):
   230         self.r = r
   231         self.g = g
   232         self.b = b
   233     def set(self, r, g, b):
   234         self.r = r
   235         self.g = g
   236         self.b = b
   237 class Colors:
   238     # XXX add more
   239     default_colors = [Color(1, 0, 0), Color(0, 1, 0), Color(0, 0, 1), Color(1, 1, 0), Color(1, 0, 1), Color(0, 1, 1)]
   240     def __init__(self):
   241         self.__colors = {}
   242     def add(self, name, color):
   243         self.__colors[name] = color
   244     def lookup(self, name):
   245         if not self.__colors.has_key(name):
   246             self.add(name, self.default_colors.pop())
   247         return self.__colors.get(name)
   248 
   249 
   250 class TopLegendRenderer:
   251     def __init__(self):
   252         self.__padding = 10
   253     def set_padding(self, padding):
   254         self.__padding = padding
   255     def set_legends(self, legends, colors):
   256         self.__legends = legends
   257         self.__colors = colors
   258     def layout(self, width):
   259         self.__width = width
   260         surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
   261         ctx = cairo.Context(surface)
   262         line_height = 0
   263         total_height = self.__padding
   264         line_used = self.__padding
   265         for legend in self.__legends:
   266             (t_width, t_height) = ctx.text_extents(legend)[2:4]
   267             item_width = self.__padding + self.__padding + t_width + self.__padding
   268             item_height = t_height + self.__padding
   269             if item_height > line_height:
   270                 line_height = item_height
   271             if line_used + item_width > self.__width:
   272                 line_used = self.__padding + item_width
   273                 total_height += line_height
   274             else:
   275                 line_used += item_width
   276             x = line_used - item_width
   277         total_height += line_height
   278         self.__height = total_height
   279 
   280     def get_height(self):
   281         return self.__height
   282     def draw(self, ctx):
   283         i = 0
   284         line_height = 0
   285         total_height = self.__padding
   286         line_used = self.__padding
   287         for legend in self.__legends:
   288             (t_width, t_height) = ctx.text_extents(legend)[2:4]
   289             item_width = self.__padding + self.__padding + t_width + self.__padding
   290             item_height = t_height + self.__padding
   291             if item_height > line_height:
   292                 line_height = item_height
   293             if line_used + item_width > self.__width:
   294                 line_used = self.__padding + item_width
   295                 total_height += line_height
   296             else:
   297                 line_used += item_width
   298             x = line_used - item_width
   299             ctx.rectangle(x, total_height, self.__padding, self.__padding)
   300             ctx.set_source_rgb(0, 0, 0)
   301             ctx.set_line_width(2)
   302             ctx.stroke_preserve()
   303             ctx.set_source_rgb(self.__colors[i].r, 
   304                                self.__colors[i].g, 
   305                                self.__colors[i].b)
   306             ctx.fill()
   307             ctx.move_to(x + self.__padding*2, total_height + t_height)
   308             ctx.set_source_rgb(0, 0, 0)
   309             ctx.show_text(legend)
   310             i += 1
   311 
   312         return
   313 
   314 class TimelinesRenderer:
   315     def __init__(self):
   316         self.padding = 10
   317         return
   318     def get_height(self):
   319         return self.height
   320     def set_timelines(self, timelines, colors):
   321         self.timelines = timelines
   322         self.colors = colors
   323     def set_render_range(self, start, end):
   324         self.start = start
   325         self.end = end
   326     def get_data_x_start(self):
   327         return self.padding / 2 + self.left_width + self.padding + self.right_width + self.padding / 2
   328     def layout(self, width):
   329         surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
   330         ctx = cairo.Context(surface)
   331         max_text_height = ctx.text_extents("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]
   332 
   333         left_width = 0
   334         right_width = 0
   335         left_n_lines = 0
   336         range_n = 0
   337         eventint_n = 0
   338         eventstr_n = 0
   339         for timeline in self.timelines.get_all():
   340             left_n_lines += 1
   341             t_width = ctx.text_extents(timeline.name)[2]
   342             left_width = max(left_width, t_width)
   343             for rang in timeline.get_ranges():
   344                 t_width = ctx.text_extents(rang.name)[2]
   345                 right_width = max(right_width, t_width)
   346                 range_n += 1
   347             for events_int in timeline.get_events_int():
   348                 t_width = ctx.text_extents(events_int.name)[2]
   349                 right_width = max(right_width, t_width)
   350                 eventint_n += 1
   351             for events_str in timeline.get_events_str():
   352                 t_width = ctx.text_extents(events_str.name)[2]
   353                 right_width = max(right_width, t_width)
   354                 eventstr_n += 1
   355 
   356         left_height = left_n_lines * max_text_height + (left_n_lines - 1) * self.padding
   357         right_n_lines = range_n + eventint_n + eventstr_n
   358         right_height = (right_n_lines - 1) * self.padding + right_n_lines * max_text_height
   359         right_data_height = (eventint_n + eventstr_n) * (max_text_height + 5) + range_n * 10
   360         right_data_height += (right_n_lines - 1) * self.padding
   361 
   362         height = max(left_height, right_height)
   363         height = max(height, right_data_height)
   364 
   365         self.left_width = left_width
   366         self.right_width = right_width
   367         self.max_text_height = max_text_height
   368         self.width = width
   369         self.height = height + self.padding
   370     def draw_line(self, ctx, x, y, width, height):
   371         ctx.move_to(x, y)
   372         ctx.rel_line_to(width, height)
   373         ctx.close_path()
   374         ctx.set_operator(cairo.OPERATOR_SOURCE)
   375         ctx.set_line_width(1.0)
   376         ctx.set_source_rgb(0, 0, 0)
   377         ctx.stroke()
   378     def draw_events(self, ctx, events, x, y, width, height):
   379         if (self.grey_background % 2) == 0:
   380             ctx.rectangle(x, y - self.padding / 2, 
   381                           width, height + self.padding)
   382             ctx.set_source_rgb(0.9, 0.9, 0.9)
   383             ctx.fill()
   384         last_x_drawn = int(x)
   385         (lo, hi) = events.get_events_bounds(self.start, self.end)
   386         for event in events.events[lo:hi]:
   387             real_x = int(x + (event.at - self.start) * width / (self.end - self.start))
   388             if real_x > last_x_drawn + 2:
   389                 ctx.rectangle(real_x, y, 1, 1)
   390                 ctx.set_source_rgb(1, 0, 0)
   391                 ctx.stroke()
   392                 ctx.move_to(real_x, y + self.max_text_height)
   393                 ctx.set_source_rgb(0, 0, 0)
   394                 ctx.show_text(str(event.value))
   395                 last_x_drawn = real_x
   396         self.grey_background += 1
   397     def draw_ranges(self, ctx, ranges, x, y, width, height):
   398         if (self.grey_background % 2) == 0:
   399             ctx.rectangle(x, y - self.padding / 2, 
   400                           width, height + self.padding)
   401             ctx.set_source_rgb(0.9, 0.9, 0.9)
   402             ctx.fill()
   403         last_x_drawn = int(x - 1)
   404         (lo, hi) = ranges.get_ranges_bounds(self.start, self.end)
   405         for data_range in ranges.ranges[lo:hi]:
   406             s = max(data_range.start, self.start)
   407             e = min(data_range.end, self.end)
   408             x_start = int(x + (s - self.start) * width / (self.end - self.start))
   409             x_end = int(x + (e - self.start) * width / (self.end - self.start))
   410             if x_end > last_x_drawn:
   411                 ctx.rectangle(x_start, y, x_end - x_start, 10)
   412                 ctx.set_source_rgb(0, 0, 0)
   413                 ctx.stroke_preserve()
   414                 color = self.colors.lookup(data_range.value)
   415                 ctx.set_source_rgb(color.r, color.g, color.b)
   416                 ctx.fill()
   417                 last_x_drawn = x_end
   418 
   419         self.grey_background += 1
   420 
   421     def draw(self, ctx):
   422         timeline_top = 0
   423         top_y = self.padding / 2
   424         left_x_start = self.padding / 2
   425         left_x_end = left_x_start + self.left_width
   426         right_x_start = left_x_end + self.padding
   427         right_x_end = right_x_start + self.right_width
   428         data_x_start = right_x_end + self.padding / 2
   429         data_x_end = self.width
   430         data_width = data_x_end - data_x_start
   431         cur_y = top_y
   432         self.draw_line(ctx, 0, 0, self.width, 0)
   433         self.grey_background = 1
   434         for timeline in self.timelines.get_all():
   435             (y_bearing, t_width, t_height) = ctx.text_extents(timeline.name)[1:4]
   436             ctx.move_to(left_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
   437             ctx.show_text(timeline.name);
   438             for events_int in timeline.get_events_int():
   439                 (y_bearing, t_width, t_height) = ctx.text_extents(events_int.name)[1:4]
   440                 ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
   441                 ctx.show_text(events_int.name)
   442                 self.draw_events(ctx, events_int, data_x_start, cur_y, data_width, self.max_text_height + 5)
   443                 cur_y += self.max_text_height + 5 + self.padding
   444                 self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, 
   445                                self.right_width + self.padding, 0)
   446 
   447             for events_str in timeline.get_events_str():
   448                 (y_bearing, t_width, t_height) = ctx.text_extents(events_str.name)[1:4]
   449                 ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
   450                 ctx.show_text(events_str.name)
   451                 self.draw_events(ctx, events_str, data_x_start, cur_y, data_width, self.max_text_height + 5)
   452                 cur_y += self.max_text_height + 5 + self.padding
   453                 self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, 
   454                                self.right_width + self.padding, 0)
   455             for ranges in timeline.get_ranges():
   456                 (y_bearing, t_width, t_height) = ctx.text_extents(ranges.name)[1:4]
   457                 ctx.move_to(right_x_start, cur_y + self.max_text_height - (t_height + y_bearing))
   458                 ctx.show_text(ranges.name)
   459                 self.draw_ranges(ctx, ranges, data_x_start, cur_y, data_width, 10)
   460                 cur_y += self.max_text_height + self.padding
   461                 self.draw_line(ctx, right_x_start - self.padding / 2, cur_y - self.padding / 2, 
   462                                self.right_width + self.padding, 0)
   463             self.draw_line(ctx, 0, cur_y - self.padding / 2, 
   464                            self.width, 0)
   465         bot_y = cur_y - self.padding / 2
   466         self.draw_line(ctx, left_x_end + self.padding / 2, 0, 
   467                        0, bot_y)
   468         self.draw_line(ctx, right_x_end + self.padding / 2, 0, 
   469                        0, bot_y)
   470         return
   471 
   472 class ScaleRenderer:
   473     def __init__(self):
   474         self.__top = 0
   475         return
   476     def set_bounds(self, lo, hi):
   477         self.__lo = lo
   478         self.__hi = hi
   479     def get_position(self, x):
   480         real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
   481         return real_x
   482     def set_top(self):
   483         self.__top = 1
   484     def set_bot(self):
   485         self.__top = 0
   486     def layout(self, width):
   487         surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
   488         ctx = cairo.Context(surface)
   489 
   490         # calculate scale delta
   491         data_delta = self.__hi - self.__lo
   492         closest = 1
   493         while (closest*10) < data_delta:
   494             closest *= 10
   495         if (data_delta / closest) == 0:
   496             delta = closest
   497         elif(data_delta / closest) == 1:
   498             delta = closest / 10
   499         else:
   500             delta = closest
   501         start = self.__lo - (self.__lo % delta) + delta
   502         end = self.__hi - (self.__hi % delta)
   503 
   504         self.__delta = delta
   505         self.__width = width
   506 
   507         # calculate text height
   508         max_text_height = ctx.text_extents("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]
   509         self.max_text_height = max_text_height
   510         height = max_text_height + 10
   511         self.__height = height
   512 
   513     def get_height(self):
   514         return self.__height
   515     def draw(self, ctx):
   516         delta = self.__delta
   517         start = self.__lo - (self.__lo % delta) + delta
   518         end = self.__hi - (self.__hi % delta)
   519 
   520         if self.__top == 1:
   521             s = -1
   522         else:
   523             s = 1
   524         # print scale points
   525         ctx.set_source_rgb(0, 0, 0)
   526         ctx.set_line_width(1.0)
   527         ticks = range(int(start), int(end + delta), int(delta))
   528         for x in ticks:
   529             real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
   530             ctx.move_to(real_x, 0)
   531             ctx.line_to(real_x, 5*s)
   532             ctx.close_path()
   533             ctx.stroke()
   534             (t_y_bearing, t_width, t_height) = ctx.text_extents(str(x))[1:4]
   535             if self.__top:
   536                 text_delta = t_height + t_y_bearing
   537             else:
   538                 text_delta = -t_y_bearing
   539             ctx.move_to(real_x - t_width / 2, (5 + 5 + text_delta)*s)
   540             ctx.show_text(str(x))
   541         # draw subticks
   542         delta /= 10
   543         if delta > 0:
   544             start = self.__lo - (self.__lo % delta) + delta
   545             end = self.__hi - (self.__hi % delta)
   546             for x in range(int(start), int(end + delta), int(delta)):
   547                 real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
   548                 ctx.move_to(real_x, 0)
   549                 ctx.line_to(real_x, 3*s)
   550                 ctx.close_path()
   551                 ctx.stroke()
   552 
   553 
   554 
   555 class GraphicRenderer:
   556     def __init__(self, start, end):
   557         self.__start = float(start)
   558         self.__end = float(end)
   559         self.__mid_scale = ScaleRenderer()
   560         self.__mid_scale.set_top()
   561         self.__bot_scale = ScaleRenderer()
   562         self.__bot_scale.set_bounds(start, end)
   563         self.__bot_scale.set_bot()
   564         self.__width = 1
   565         self.__height = 1
   566     def get_width(self):
   567         return self.__width
   568     def get_height(self):
   569         return self.__height
   570     # return x, y, width, height
   571     def get_data_rectangle(self):
   572         y_start = self.__top_legend.get_height()
   573         x_start = self.__data.get_data_x_start()
   574         return(x_start, y_start, self.__width - x_start, self.__data.get_height())
   575     def scale_data(self, x):
   576         x_start = self.__data.get_data_x_start()
   577         x_scaled = x / (self.__width - x_start) * (self.__r_end - self.__r_start)
   578         return x_scaled
   579     # return x, y, width, height
   580     def get_selection_rectangle(self):
   581         y_start = self.__top_legend.get_height() + self.__data.get_height() + self.__mid_scale.get_height() + 20
   582         y_height = self.__bot_scale.get_height() + 20
   583         x_start = self.__bot_scale.get_position(self.__r_start)
   584         x_end = self.__bot_scale.get_position(self.__r_end)
   585         return(x_start, y_start, x_end - x_start, y_height)
   586     def scale_selection(self, x):
   587         x_scaled = x / self.__width * (self.__end - self.__start)
   588         return x_scaled
   589     def set_range(self, start, end):
   590         s = min(start, end)
   591         e = max(start, end)
   592         start = max(self.__start, s)
   593         end = min(self.__end, e)
   594         self.__r_start = start
   595         self.__r_end = end
   596         self.__data.set_render_range(start, end)
   597         self.__mid_scale.set_bounds(start, end)
   598         self.layout(self.__width, self.__height)
   599     def get_range(self):
   600         return(self.__r_start, self.__r_end)
   601     def set_data(self, data):
   602         self.__data = data
   603     def set_top_legend(self, top_legend):
   604         self.__top_legend = top_legend
   605     def layout(self, width, height):
   606         self.__width = width
   607         self.__height = height
   608         self.__top_legend.layout(width)
   609         top_legend_height = self.__top_legend.get_height()
   610         self.__data.layout(width)
   611         self.__mid_scale.layout(width - self.__data.get_data_x_start())
   612         self.__bot_scale.layout(width)
   613         return
   614     def __x_pixel(self, x, width):
   615         new_x = (x - self.__start) * width / (self.__end - self.__start)
   616         return new_x
   617 
   618     def draw(self, ctx):
   619         # default background is white
   620         ctx.save()
   621         ctx.set_source_rgb(1, 1, 1)
   622         ctx.set_operator(cairo.OPERATOR_SOURCE)
   623         ctx.rectangle(0, 0, self.__width, self.__height)
   624         ctx.fill()
   625 
   626         # top legend
   627         ctx.save()
   628         self.__top_legend.draw(ctx)
   629         top_legend_height = self.__top_legend.get_height()
   630         ctx.restore()
   631 
   632         # separation line
   633         ctx.move_to(0, top_legend_height)
   634         ctx.line_to(self.__width, top_legend_height)
   635         ctx.close_path()
   636         ctx.set_line_width(2)
   637         ctx.set_source_rgb(0, 0, 0)
   638         ctx.stroke()
   639 
   640         # data
   641         ctx.save()
   642         ctx.translate(0, 
   643                        top_legend_height)
   644         self.__data.draw(ctx)
   645         ctx.restore()
   646 
   647         # scale below data
   648         ctx.save()
   649         ctx.translate(self.__data.get_data_x_start(), 
   650                        top_legend_height + self.__data.get_height() + self.__mid_scale.get_height())
   651         self.__mid_scale.draw(ctx)
   652         ctx.restore()
   653 
   654         height_used = top_legend_height + self.__data.get_height() + self.__mid_scale.get_height()
   655 
   656         # separation between scale and left pane
   657         ctx.move_to(self.__data.get_data_x_start(), height_used)
   658         ctx.rel_line_to(0, -self.__mid_scale.get_height())
   659         ctx.close_path()
   660         ctx.set_source_rgb(0, 0, 0)
   661         ctx.set_line_width(2)
   662         ctx.stroke()
   663 
   664         # separation below scale
   665         ctx.move_to(0, height_used)
   666         ctx.line_to(self.__width, height_used)
   667         ctx.close_path()
   668         ctx.set_line_width(2)
   669         ctx.set_source_rgb(0, 0, 0)
   670         ctx.stroke()
   671 
   672         select_start = self.__bot_scale.get_position(self.__r_start)
   673         select_end = self.__bot_scale.get_position(self.__r_end)
   674 
   675         # left connection between top scale and bottom scale
   676         ctx.move_to(0, height_used);
   677         ctx.line_to(self.__data.get_data_x_start(), height_used)
   678         ctx.line_to(select_start, height_used + 20)
   679         ctx.line_to(0, height_used + 20)
   680         ctx.line_to(0, height_used)
   681         ctx.set_source_rgb(0, 0, 0)
   682         ctx.set_line_width(1)
   683         ctx.stroke_preserve()
   684         ctx.set_source_rgb(0.9, 0.9, 0.9)
   685         ctx.fill()
   686 
   687         # right connection between top scale and bottom scale
   688         ctx.move_to(self.__width, height_used)
   689         ctx.line_to(self.__width, height_used + 20)
   690         ctx.line_to(select_end, height_used + 20)
   691         ctx.line_to(self.__width, height_used)
   692         ctx.set_source_rgb(0, 0, 0)
   693         ctx.set_line_width(1)
   694         ctx.stroke_preserve()
   695         ctx.set_source_rgb(0.9, 0.9, 0.9)
   696         ctx.fill()
   697 
   698         height_used += 20
   699 
   700         # unused area background
   701         unused_start = self.__bot_scale.get_position(self.__r_start)
   702         unused_end = self.__bot_scale.get_position(self.__r_end)
   703         unused_height = self.__bot_scale.get_height() + 20
   704         ctx.rectangle(0, height_used, 
   705                        unused_start, 
   706                        unused_height)
   707         ctx.rectangle(unused_end, 
   708                        height_used, 
   709                        self.__width - unused_end, 
   710                        unused_height)
   711         ctx.set_source_rgb(0.9, 0.9, 0.9)
   712         ctx.fill()
   713 
   714         # border line around bottom scale
   715         ctx.move_to(unused_end, height_used)
   716         ctx.line_to(self.__width, height_used)
   717         ctx.line_to(self.__width, height_used + unused_height)
   718         ctx.line_to(0, height_used + unused_height)
   719         ctx.line_to(0, height_used)
   720         ctx.line_to(unused_start, height_used)
   721         ctx.close_path()
   722         ctx.set_line_width(2)
   723         ctx.set_source_rgb(0, 0, 0)
   724         ctx.stroke()
   725         ctx.move_to(unused_start, height_used)
   726         ctx.line_to(unused_end, height_used)
   727         ctx.close_path()
   728         ctx.set_line_width(1)
   729         ctx.set_source_rgb(0.9, 0.9, 0.9)
   730         ctx.stroke()
   731 
   732         # unused area dot borders
   733         ctx.save()
   734         ctx.move_to(max(unused_start, 2), height_used)
   735         ctx.rel_line_to(0, unused_height)
   736         ctx.move_to(min(unused_end, self.__width - 2), height_used)
   737         ctx.rel_line_to(0, unused_height)
   738         ctx.set_dash([5], 0)
   739         ctx.set_source_rgb(0, 0, 0)
   740         ctx.set_line_width(1)
   741         ctx.stroke()
   742         ctx.restore()
   743 
   744         # bottom scale
   745         ctx.save()
   746         ctx.translate(0, height_used)
   747         self.__bot_scale.draw(ctx)
   748         ctx.restore()
   749 
   750 class GtkGraphicRenderer(gtk.DrawingArea):
   751     def __init__(self, data):
   752         super(GtkGraphicRenderer, self).__init__()
   753         self.__data = data
   754         self.__moving_left = False
   755         self.__moving_right = False
   756         self.__moving_both = False
   757         self.__moving_top = False
   758         self.__force_full_redraw = True
   759         self.add_events(gtk.gdk.POINTER_MOTION_MASK)
   760         self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
   761         self.add_events(gtk.gdk.BUTTON_RELEASE_MASK)
   762         self.connect("expose_event", self.expose)
   763         self.connect('size-allocate', self.size_allocate)
   764         self.connect('motion-notify-event', self.motion_notify)
   765         self.connect('button-press-event', self.button_press)
   766         self.connect('button-release-event', self.button_release)
   767     def set_smaller_zoom(self):
   768         (start, end) = self.__data.get_range()
   769         self.__data.set_range(start, start + (end - start)*2)
   770         self.__force_full_redraw = True
   771         self.queue_draw()
   772     def set_bigger_zoom(self):
   773         (start, end) = self.__data.get_range()
   774         self.__data.set_range(start, start + (end - start) / 2)
   775         self.__force_full_redraw = True
   776         self.queue_draw()
   777     def output_png(self, filename):
   778         surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 
   779                                      self.__data.get_width(), 
   780                                      self.__data.get_height())
   781         ctx = cairo.Context(self.__buffer_surface)
   782         self.__data.draw(ctx)
   783         surface.write_to_png(filename)
   784     def button_press(self, widget, event):
   785         (x, y, width, height) = self.__data.get_selection_rectangle()
   786         (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle()
   787         if event.y > y and event.y < y + height:
   788             if abs(event.x - x) < 5:
   789                 self.__moving_left = True
   790                 return True
   791             if abs(event.x - (x + width)) < 5:
   792                 self.__moving_right = True
   793                 return True
   794             if event.x > x and event.x < x + width:
   795                 self.__moving_both = True
   796                 self.__moving_both_start = event.x
   797                 self.__moving_both_cur = event.x
   798                 return True
   799         if event.y > d_y and event.y < (d_y + d_height):
   800             if event.x > d_x and event.x < (d_x + d_width):
   801                 self.__moving_top = True
   802                 self.__moving_top_start = event.x
   803                 self.__moving_top_cur = event.x
   804                 return True
   805         return False
   806     def button_release(self, widget, event):
   807         if self.__moving_left:
   808             self.__moving_left = False
   809             left = self.__data.scale_selection(self.__moving_left_cur)
   810             right = self.__data.get_range()[1]
   811             self.__data.set_range(left, right)
   812             self.__force_full_redraw = True
   813             self.queue_draw()
   814             return True
   815         if self.__moving_right:
   816             self.__moving_right = False
   817             right = self.__data.scale_selection(self.__moving_right_cur)
   818             left = self.__data.get_range()[0]
   819             self.__data.set_range(left, right)
   820             self.__force_full_redraw = True
   821             self.queue_draw()
   822             return True
   823         if self.__moving_both:
   824             self.__moving_both = False
   825             delta = self.__data.scale_selection(self.__moving_both_cur - self.__moving_both_start)
   826             (left, right) = self.__data.get_range()
   827             self.__data.set_range(left + delta, right + delta)
   828             self.__force_full_redraw = True
   829             self.queue_draw()
   830             return True
   831         if self.__moving_top:
   832             self.__moving_top = False
   833         return False
   834     def motion_notify(self, widget, event):
   835         (x, y, width, height) = self.__data.get_selection_rectangle()
   836         if self.__moving_left:
   837             if event.x <= 0:
   838                 self.__moving_left_cur = 0
   839             elif event.x >= x + width:
   840                 self.__moving_left_cur = x + width
   841             else:
   842                 self.__moving_left_cur = event.x
   843             self.queue_draw_area(0, int(y), int(self.__width), int(height))
   844             return True
   845         if self.__moving_right:
   846             if event.x >= self.__width:
   847                 self.__moving_right = self.__width
   848             elif event.x < x:
   849                 self.__moving_right_cur = x
   850             else:
   851                 self.__moving_right_cur = event.x
   852             self.queue_draw_area(0, int(y), int(self.__width), int(height))
   853             return True
   854         if self.__moving_both:
   855             cur_e = self.__width - (x + width - self.__moving_both_start)
   856             cur_s = (self.__moving_both_start - x)
   857             if event.x < cur_s:
   858                 self.__moving_both_cur = cur_s
   859             elif event.x > cur_e:
   860                 self.__moving_both_cur = cur_e
   861             else:
   862                 self.__moving_both_cur = event.x
   863             self.queue_draw_area(0, int(y), int(self.__width), int(height))
   864             return True
   865         if self.__moving_top:
   866             self.__moving_top_cur = event.x
   867             delta = self.__data.scale_data(self.__moving_top_start - self.__moving_top_cur)
   868             (left, right) = self.__data.get_range()
   869             self.__data.set_range(left + delta, right + delta)
   870             self.__force_full_redraw = True
   871             self.__moving_top_start = event.x
   872             self.queue_draw()
   873             return True
   874         (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle()
   875         if event.y > y and event.y < y + height:
   876             if abs(event.x - x) < 5 or abs(event.x - (x + width)) < 5:
   877                 widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.SB_H_DOUBLE_ARROW))
   878                 return True
   879             if event.x > x and event.x < x + width:
   880                 widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
   881                 return True
   882         if event.y > d_y and event.y < (d_y + d_height):
   883             if event.x > d_x and event.x < (d_x + d_width):
   884                 widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
   885                 return True
   886         widget.window.set_cursor(None)
   887         return False
   888     def size_allocate(self, widget, allocation):
   889         self.__width = allocation.width
   890         self.__height = allocation.height
   891         self.__data.layout(allocation.width, allocation.height)
   892         self.__force_full_redraw = True
   893         self.queue_draw()
   894     def expose(self, widget, event):
   895         if self.__force_full_redraw:
   896             self.__buffer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 
   897                                                        self.__data.get_width(), 
   898                                                        self.__data.get_height())
   899             ctx = cairo.Context(self.__buffer_surface)
   900             self.__data.draw(ctx)
   901             self.__force_full_redraw = False
   902         ctx = widget.window.cairo_create()
   903         ctx.rectangle(event.area.x, event.area.y, 
   904                       event.area.width, event.area.height)
   905         ctx.clip()
   906         ctx.set_source_surface(self.__buffer_surface)
   907         ctx.paint()
   908         (x, y, width, height) = self.__data.get_selection_rectangle()
   909         if self.__moving_left:
   910             ctx.move_to(max(self.__moving_left_cur, 2), y)
   911             ctx.rel_line_to(0, height)
   912             ctx.close_path()
   913             ctx.set_line_width(1)
   914             ctx.set_source_rgb(0, 0, 0)
   915             ctx.stroke()
   916         if self.__moving_right:
   917             ctx.move_to(min(self.__moving_right_cur, self.__width - 2), y)
   918             ctx.rel_line_to(0, height)
   919             ctx.close_path()
   920             ctx.set_line_width(1)
   921             ctx.set_source_rgb(0, 0, 0)
   922             ctx.stroke()
   923         if self.__moving_both:
   924             delta_x = self.__moving_both_cur - self.__moving_both_start
   925             left_x = x + delta_x
   926             ctx.move_to(x + delta_x, y)
   927             ctx.rel_line_to(0, height)
   928             ctx.close_path()
   929             ctx.move_to(x + width + delta_x, y)
   930             ctx.rel_line_to(0, height)
   931             ctx.close_path()
   932             ctx.set_source_rgb(0, 0, 0)
   933             ctx.set_line_width(1)
   934             ctx.stroke()
   935         return False
   936 
   937 class MainWindow:
   938     def __init__(self):
   939         return
   940     def run(self, graphic):
   941         window = gtk.Window()
   942         self.__window = window
   943         window.set_default_size(200, 200)
   944         vbox = gtk.VBox()
   945         window.add(vbox)
   946         render = GtkGraphicRenderer(graphic)
   947         self.__render = render
   948         vbox.pack_end(render, True, True, 0)
   949         hbox = gtk.HBox()
   950         vbox.pack_start(hbox, False, False, 0)
   951         smaller_zoom = gtk.Button("Zoom Out")
   952         smaller_zoom.connect("clicked", self.__set_smaller_cb)
   953         hbox.pack_start(smaller_zoom)
   954         bigger_zoom = gtk.Button("Zoom In")
   955         bigger_zoom.connect("clicked", self.__set_bigger_cb)
   956         hbox.pack_start(bigger_zoom)
   957         output_png = gtk.Button("Output Png")
   958         output_png.connect("clicked", self.__output_png_cb)
   959         hbox.pack_start(output_png)
   960         window.connect('destroy', gtk.main_quit)
   961         window.show_all()
   962         #gtk.bindings_activate(gtk.main_quit, 'q', 0)
   963         gtk.main()
   964     def __set_smaller_cb(self, widget):
   965         self.__render.set_smaller_zoom()
   966     def __set_bigger_cb(self, widget):
   967         self.__render.set_bigger_zoom()
   968     def __output_png_cb(self, widget):
   969         dialog = gtk.FileChooserDialog("Output Png", self.__window, 
   970                                        gtk.FILE_CHOOSER_ACTION_SAVE, ("Save", 1))
   971         self.__dialog = dialog
   972         dialog.set_default_response(1)
   973         dialog.connect("response", self.__dialog_response_cb)
   974         dialog.show()
   975         return
   976     def __dialog_response_cb(self, widget, response):
   977         if response == 1:
   978             filename = self.__dialog.get_filename()
   979             self.__render.output_png(filename)
   980             widget.hide()
   981         return
   982 
   983 
   984 
   985 def read_data(filename):
   986     timelines = Timelines()
   987     colors = Colors()
   988     fh = open(filename)
   989     m1 = re.compile('range ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
   990     m2 = re.compile('event-str ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)')
   991     m3 = re.compile('event-int ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
   992     m4 = re.compile('color ([^ ]+) #([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})')
   993     for line in fh.readlines():
   994         m = m1.match(line)
   995         if m:
   996             line_name = m.group(1)
   997             timeline = timelines.get(m.group(1))
   998             rang = timeline.get_range(m.group(2))
   999             data_range = DataRange()
  1000             data_range.value = m.group(3)
  1001             data_range.start = int(m.group(4))
  1002             data_range.end = int(m.group(5))
  1003             rang.add_range(data_range)
  1004             continue
  1005         m = m2.match(line)
  1006         if m:
  1007             line_name = m.group(1)
  1008             timeline = timelines.get(m.group(1))
  1009             ev = timeline.get_event_str(m.group(2))
  1010             event = EventString()
  1011             event.value = m.group(3)
  1012             event.at = int(m.group(4))
  1013             ev.add_event(event)
  1014             continue
  1015         m = m3.match(line)
  1016         if m:
  1017             line_name = m.group(1)
  1018             timeline = timelines.get(m.group(1))
  1019             ev = timeline.get_event_int(m.group(2))
  1020             event = EventInt()
  1021             event.value = int(m.group(3))
  1022             event.at = int(m.group(4))
  1023             ev.add_event(event)
  1024             continue
  1025 
  1026         m = m4.match(line)
  1027         if m:
  1028             r = int(m.group(2), 16)
  1029             g = int(m.group(3), 16)
  1030             b = int(m.group(4), 16)
  1031             color = Color(r / 255, g / 255, b / 255)
  1032             colors.add(m.group(1), color)
  1033             continue
  1034     timelines.sort()
  1035     return (colors, timelines)
  1036 
  1037 
  1038 
  1039 def main():
  1040     (colors, timelines) = read_data(sys.argv[1])
  1041     (lower_bound, upper_bound) = timelines.get_bounds()
  1042     graphic = GraphicRenderer(lower_bound, upper_bound)
  1043     top_legend = TopLegendRenderer()
  1044     range_values = timelines.get_all_range_values()
  1045     range_colors = []
  1046     for range_value in range_values:
  1047         range_colors.append(colors.lookup(range_value))
  1048     top_legend.set_legends(range_values, 
  1049                            range_colors)
  1050     graphic.set_top_legend(top_legend)
  1051     data = TimelinesRenderer()
  1052     data.set_timelines(timelines, colors)
  1053     graphic.set_data(data)
  1054 
  1055     # default range
  1056     range_mid = (upper_bound - lower_bound) / 2
  1057     range_width = (upper_bound - lower_bound) / 10
  1058     range_lo = range_mid - range_width / 2
  1059     range_hi = range_mid + range_width / 2
  1060     graphic.set_range(range_lo, range_hi)
  1061 
  1062     main_window = MainWindow()
  1063     main_window.run(graphic)
  1064 
  1065 
  1066 main()