utils/grid.py
author Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
Fri, 06 Oct 2006 13:37:25 +0200
changeset 122 6b8f1eda5c57
parent 12 917ba023c576
child 124 c0d0f7bebb73
permissions -rwxr-xr-x
fix coding style

#!/usr/bin/env python

import cairo
import sys
import re
import gtk



class DataRange:
    def __init__ (self, start = 0, end = 0, value = ''):
        self.start = start
        self.end = end
        self.value = value
class EventString:
    def __init__ (self, at = 0, value = ''):
        self.at = at
        self.value = value
class EventFloat:
    def __init__ (self, at = 0, value = 0.0):
        self.at = at
        self.value = value
class EventInt:
    def __init__ (self, at = 0, value = 0.0):
        self.at = at
        self.value = value
def ranges_cmp (a, b):
    return a.start - b.start
def events_cmp (a,b):
    return a.at - b.at
class TimelineDataRange:
    def __init__ (self, name = ''):
        self.name = name
        self.ranges = []
        return
    def __search (self, key):
        l = 0
        u = len (self.ranges)-1
        while l <= u:
            i = int ((l+u)/2)
            if key >= self.ranges[i].start and key <= self.ranges[i].end:
                return i
            elif key < self.ranges[i].start:
                u = i - 1
            else:
                # key > self.ranges[i].end
                l = i + 1
        return -1
    def add_range (self, range):
        self.ranges.append (range)
    def get_all (self):
        return self.ranges
    def get_ranges (self, start, end):
        s = self.__search (start)
        e = self.__search (end)
        if s == -1 and e == -1:
            return []
        elif s == -1:
            return self.ranges[0:e+1]
        elif e == -1:
            return self.ranges[s:len (self.ranges)]
        else:
            return self.ranges[s:e+1]
    def get_ranges_bounds (self, start, end):
        s = self.__search (start)
        e = self.__search (end)
        if s == -1 and e == -1:
            return (0,0)
        elif s == -1:
            return (0,e+1)
        elif e == -1:
            return (s, len (self.ranges))
        else:
            return (s,e+1)
    def sort (self):
        self.ranges.sort (ranges_cmp)
    def get_bounds (self):
        if len (self.ranges) > 0:
            lo = self.ranges[0].start
            hi = self.ranges[len (self.ranges)-1].end
            return (lo, hi)
        else:
            return (0,0)
class TimelineEvent:
    def __init__ (self, name = ''):
        self.name = name
        self.events = []
    def __search (self, key):
        l = 0
        u = len (self.events)-1
        while l <= u:
            i = int ((l+u)/2)
            if key == self.events[i].at:
                return i
            elif key < self.events[i].at:
                u = i - 1
            else:
                # key > self.events[i].at
                l = i + 1
        return l
    def add_event (self, event):
        self.events.append (event)
    def get_events (self, start, end):
        s = self.__search (start)
        e = self.__search (end)
        return self.events[s:e+1]
    def get_events_bounds (self, start, end):
        s = self.__search (start)
        e = self.__search (end)
        return (s, e+1)
    def sort (self):
        self.events.sort (events_cmp)
    def get_bounds (self):
        if len (self.events) > 0:
            lo = self.events[0].at
            hi = self.events[-1].at
            return (lo,hi)
        else:
            return (0,0)

class Timeline:
    def __init__ (self, name = ''):
        self.ranges = []
        self.event_str = []
        self.event_int = []
        self.name = name
    def get_range (self, name):
        for range in self.ranges:
            if range.name == name:
                return range
        timeline = TimelineDataRange (name)
        self.ranges.append (timeline)
        return timeline
    def get_event_str (self, name):
        for event_str in self.event_str:
            if event_str.name == name:
                return event_str
        timeline = TimelineEvent (name)
        self.event_str.append (timeline)
        return timeline
    def get_event_int (self, name):
        for event_int in self.event_int:
            if event_int.name == name:
                return event_int
        timeline = TimelineEvent (name)
        self.event_int.append (timeline)
        return timeline
    def get_ranges (self):
        return self.ranges
    def get_events_str (self):
        return self.event_str
    def get_events_int (self):
        return self.event_int
    def sort (self):
        for range in self.ranges:
            range.sort ()
        for event in self.event_int:
            event.sort ()
        for event in self.event_str:
            event.sort ()
    def get_bounds (self):
        lo = 0
        hi = 0
        for range in self.ranges:
            (range_lo, range_hi) = range.get_bounds ()
            if range_lo < lo:
                lo = range_lo
            if range_hi > hi:
                hi = range_hi
        for event_str in self.event_str:
            (ev_lo, ev_hi) = event_str.get_bounds ()
            if ev_lo < lo:
                lo = ev_lo
            if ev_hi > hi:
                hi = ev_hi
        for event_int in self.event_int:
            (ev_lo, ev_hi) = event_int.get_bounds ()
            if ev_lo < lo:
                lo = ev_lo
            if ev_hi > hi:
                hi = ev_hi
        return (lo, hi)
class Timelines:
    def __init__ (self):
        self.timelines = []
    def get (self, name):
        for timeline in self.timelines:
            if timeline.name == name:
                return timeline
        timeline = Timeline (name)
        self.timelines.append (timeline)
        return timeline
    def get_all (self):
        return self.timelines
    def sort (self):
        for timeline in self.timelines:
            timeline.sort ()
    def get_bounds (self):
        lo = 0
        hi = 0
        for timeline in self.timelines:
            (t_lo, t_hi) = timeline.get_bounds ()
            if t_lo < lo:
                lo = t_lo
            if t_hi > hi:
                hi = t_hi
        return (lo, hi)
    def get_all_range_values (self):
        range_values = {}
        for timeline in self.timelines:
            for ranges in timeline.get_ranges ():
                for ran in ranges.get_all ():
                    range_values[ran.value] = 1
        return range_values.keys ()
class Color:
    def __init__ (self, r = 0.0, g = 0.0, b = 0.0):
        self.r = r
        self.g = g
        self.b = b
    def set (self, r, g, b):
        self.r = r
        self.g = g
        self.b = b
class Colors:
    # XXX add more
    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)]
    def __init__ (self):
        self.__colors = {}
    def add (self, name, color):
        self.__colors[name] = color
    def lookup (self, name):
        if not self.__colors.has_key (name):
            self.add (name, self.default_colors.pop ())
        return self.__colors.get(name)


class TopLegendRenderer:
    def __init__ (self):
        self.__padding = 10
    def set_padding (self, padding):
        self.__padding = padding
    def set_legends (self, legends, colors):
        self.__legends = legends
        self.__colors = colors
    def layout (self, width):
        self.__width = width
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
        ctx = cairo.Context(surface)
        line_height = 0
        total_height = self.__padding
        line_used = self.__padding
        for legend in self.__legends:
            (t_width, t_height) = ctx.text_extents (legend)[2:4]
            item_width = self.__padding +  self.__padding + t_width +  self.__padding
            item_height = t_height + self.__padding
            if item_height > line_height:
                line_height = item_height
            if line_used + item_width > self.__width:
                line_used = self.__padding + item_width
                total_height += line_height
            else:
                line_used += item_width
            x = line_used - item_width
        total_height += line_height
        self.__height = total_height
            
    def get_height (self):
        return self.__height
    def draw (self, ctx):
        i = 0
        line_height = 0
        total_height = self.__padding
        line_used = self.__padding
        for legend in self.__legends:
            (t_width, t_height) = ctx.text_extents (legend)[2:4]
            item_width = self.__padding +  self.__padding + t_width +  self.__padding
            item_height = t_height + self.__padding
            if item_height > line_height:
                line_height = item_height
            if line_used + item_width > self.__width:
                line_used = self.__padding + item_width
                total_height += line_height
            else:
                line_used += item_width
            x = line_used - item_width
            ctx.rectangle (x, total_height, self.__padding, self.__padding)
            ctx.set_source_rgb (0,0,0)
            ctx.set_line_width (2)
            ctx.stroke_preserve ()
            ctx.set_source_rgb (self.__colors[i].r,
                                self.__colors[i].g,
                                self.__colors[i].b)
            ctx.fill ()
            ctx.move_to (x+self.__padding*2, total_height+t_height)
            ctx.set_source_rgb (0,0,0)
            ctx.show_text (legend)
            i += 1

        return

class TimelinesRenderer:
    def __init__ (self):
        self.padding = 10
        return
    def get_height (self):
        return self.height
    def set_timelines (self, timelines, colors):
        self.timelines = timelines
        self.colors = colors
    def set_render_range (self, start, end):
        self.start = start
        self.end = end
    def get_data_x_start (self):
        return self.padding / 2 + self.left_width + self.padding + self.right_width + self.padding/2
    def layout (self, width):
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
        ctx = cairo.Context(surface)
        max_text_height = ctx.text_extents ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]

        left_width = 0
        right_width = 0
        left_n_lines = 0
        range_n = 0
        eventint_n = 0
        eventstr_n = 0
        for timeline in self.timelines.get_all ():
            left_n_lines += 1
            t_width = ctx.text_extents (timeline.name)[2]
            left_width = max (left_width, t_width)
            for rang in timeline.get_ranges ():
                t_width = ctx.text_extents (rang.name)[2]
                right_width = max (right_width, t_width)
                range_n += 1
            for events_int in timeline.get_events_int ():
                t_width = ctx.text_extents (events_int.name)[2]
                right_width = max (right_width, t_width)
                eventint_n += 1
            for events_str in timeline.get_events_str ():
                t_width = ctx.text_extents (events_str.name)[2]
                right_width = max (right_width, t_width)
                eventstr_n += 1

        left_height = left_n_lines * max_text_height + (left_n_lines - 1) * self.padding
        right_n_lines = range_n + eventint_n + eventstr_n
        right_height = (right_n_lines - 1) * self.padding + right_n_lines * max_text_height
        right_data_height = (eventint_n + eventstr_n) * (max_text_height + 5) + range_n * 10
        right_data_height += (right_n_lines - 1) * self.padding

        height = max (left_height, right_height)
        height = max (height, right_data_height)

        self.left_width = left_width
        self.right_width = right_width
        self.max_text_height = max_text_height
        self.width = width
        self.height = height + self.padding
    def draw_line (self, ctx, x, y, width, height):
        ctx.move_to (x, y)
        ctx.rel_line_to (width, height)
        ctx.close_path ()
        ctx.set_operator (cairo.OPERATOR_SOURCE)
        ctx.set_line_width (1.0)
        ctx.set_source_rgb (0,0,0)
        ctx.stroke ()
    def draw_events (self, ctx, events, x, y, width, height):
        if (self.grey_background % 2) == 0:
            ctx.rectangle (x, y-self.padding/2,
                           width, height+self.padding)
            ctx.set_source_rgb (0.9,0.9,0.9)
            ctx.fill ()
        last_x_drawn = int (x)
        (lo, hi) = events.get_events_bounds (self.start, self.end)
        for event in events.events[lo:hi]:
            real_x = int (x + (event.at - self.start) * width / (self.end - self.start))
            if real_x > last_x_drawn+2:
                ctx.rectangle (real_x, y, 1, 1)
                ctx.set_source_rgb (1,0,0)
                ctx.stroke ()
                ctx.move_to (real_x, y+self.max_text_height)
                ctx.set_source_rgb (0,0,0)
                ctx.show_text (str (event.value))
                last_x_drawn = real_x
        self.grey_background += 1
    def draw_ranges (self, ctx, ranges, x, y, width, height):
        if (self.grey_background % 2) == 0:
            ctx.rectangle (x, y-self.padding/2,
                           width, height+self.padding)
            ctx.set_source_rgb (0.9,0.9,0.9)
            ctx.fill ()
        last_x_drawn = int (x-1)
        (lo, hi) = ranges.get_ranges_bounds (self.start, self.end)
        for data_range in ranges.ranges[lo:hi]:
            s = max (data_range.start, self.start)
            e = min (data_range.end, self.end)
            x_start = int (x + (s - self.start) * width / (self.end - self.start))
            x_end = int (x + (e - self.start) * width / (self.end - self.start))
            if x_end > last_x_drawn:
                ctx.rectangle (x_start, y, x_end - x_start, 10)
                ctx.set_source_rgb (0,0,0)
                ctx.stroke_preserve ()
                color = self.colors.lookup (data_range.value)
                ctx.set_source_rgb (color.r, color.g, color.b)
                ctx.fill ()
                last_x_drawn = x_end

        self.grey_background += 1
        
    def draw (self, ctx):
        timeline_top = 0
        top_y = self.padding / 2
        left_x_start = self.padding / 2
        left_x_end = left_x_start + self.left_width
        right_x_start = left_x_end + self.padding
        right_x_end = right_x_start + self.right_width
        data_x_start = right_x_end + self.padding /2
        data_x_end = self.width
        data_width = data_x_end - data_x_start
        cur_y = top_y
        self.draw_line (ctx, 0, 0, self.width, 0)
        self.grey_background = 1
        for timeline in self.timelines.get_all ():
            (y_bearing,t_width,t_height) = ctx.text_extents (timeline.name)[1:4]
            ctx.move_to (left_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
            ctx.show_text (timeline.name);
            for events_int in timeline.get_events_int ():
                (y_bearing, t_width, t_height) = ctx.text_extents (events_int.name)[1:4]
                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
                ctx.show_text (events_int.name)
                self.draw_events (ctx, events_int, data_x_start, cur_y, data_width, self.max_text_height+5)
                cur_y += self.max_text_height + 5 + self.padding
                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
                                self.right_width + self.padding, 0)

            for events_str in timeline.get_events_str ():
                (y_bearing, t_width, t_height) = ctx.text_extents (events_str.name)[1:4]
                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
                ctx.show_text (events_str.name)
                self.draw_events (ctx, events_str, data_x_start, cur_y, data_width, self.max_text_height+5)
                cur_y += self.max_text_height + 5 + self.padding
                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
                                self.right_width + self.padding, 0)
            for ranges in timeline.get_ranges ():
                (y_bearing, t_width, t_height) = ctx.text_extents (ranges.name)[1:4]
                ctx.move_to (right_x_start, cur_y + self.max_text_height - (t_height+y_bearing))
                ctx.show_text (ranges.name)
                self.draw_ranges (ctx, ranges, data_x_start, cur_y, data_width, 10)
                cur_y += self.max_text_height + self.padding
                self.draw_line (ctx, right_x_start-self.padding/2, cur_y - self.padding / 2,
                                self.right_width + self.padding, 0)
            self.draw_line (ctx, 0, cur_y - self.padding / 2,
                            self.width, 0)
        bot_y = cur_y - self.padding / 2
        self.draw_line (ctx, left_x_end+self.padding/2, 0,
                        0, bot_y)
        self.draw_line (ctx, right_x_end+self.padding/2, 0,
                        0, bot_y)
        return

class ScaleRenderer:
    def __init__ (self):
        self.__top = 0
        return
    def set_bounds (self, lo, hi):
        self.__lo = lo
        self.__hi = hi
    def get_position (self, x):
        real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
        return real_x
    def set_top (self):
        self.__top = 1
    def set_bot (self):
        self.__top = 0
    def layout (self, width):
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1,1)
        ctx = cairo.Context(surface)

        # calculate scale delta
        data_delta = self.__hi - self.__lo
        closest = 1
        while (closest*10) < data_delta:
            closest *= 10
        if (data_delta / closest) == 0:
            delta = closest
        elif (data_delta / closest) == 1:
            delta = closest / 10
        else:
            delta = closest
        start = self.__lo - (self.__lo % delta) + delta
        end = self.__hi - (self.__hi % delta)

        self.__delta = delta
        self.__width = width

        # calculate text height
        max_text_height = ctx.text_extents ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcedefghijklmnopqrstuvwxyz0123456789")[3]
        self.max_text_height = max_text_height
        height = max_text_height + 10
        self.__height = height
    
    def get_height (self):
        return self.__height
    def draw (self, ctx):
        delta = self.__delta
        start = self.__lo - (self.__lo % delta) + delta
        end = self.__hi - (self.__hi % delta)

        if self.__top == 1:
            s = -1
        else:
            s = 1
        # print scale points
        ctx.set_source_rgb (0, 0, 0)
        ctx.set_line_width (1.0)
        ticks = range (int (start), int (end + delta), int (delta))
        for x in ticks:
            real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
            ctx.move_to (real_x, 0)
            ctx.line_to (real_x, 5*s)
            ctx.close_path ()
            ctx.stroke ()
            (t_y_bearing, t_width, t_height) = ctx.text_extents (str (x))[1:4]
            if self.__top:
                text_delta = t_height + t_y_bearing
            else:
                text_delta = -t_y_bearing
            ctx.move_to (real_x - t_width/2, (5 + 5 + text_delta)*s)
            ctx.show_text (str (x))
        # draw subticks
        delta /= 10
        if delta > 0:
            start = self.__lo - (self.__lo % delta) + delta
            end = self.__hi - (self.__hi % delta)
            for x in range (int (start), int (end + delta), int (delta)):
                real_x = (x - self.__lo ) * self.__width / (self.__hi - self.__lo)
                ctx.move_to (real_x, 0)
                ctx.line_to (real_x, 3*s)
                ctx.close_path ()
                ctx.stroke ()
        
        

class GraphicRenderer:
    def __init__(self, start, end):
        self.__start = float (start)
        self.__end = float (end)
        self.__mid_scale = ScaleRenderer ()
        self.__mid_scale.set_top ()
        self.__bot_scale = ScaleRenderer ()
        self.__bot_scale.set_bounds (start, end)
        self.__bot_scale.set_bot ()
        self.__width = 1
        self.__height = 1
    def get_width (self):
        return self.__width
    def get_height (self):
        return self.__height
    # return x, y, width, height
    def get_data_rectangle (self):
        y_start = self.__top_legend.get_height ()
        x_start = self.__data.get_data_x_start ()
        return (x_start, y_start, self.__width - x_start, self.__data.get_height ())
    def scale_data (self, x):
        x_start = self.__data.get_data_x_start ()
        x_scaled = x / (self.__width - x_start) * (self.__r_end - self.__r_start)
        return x_scaled
    # return x, y, width, height
    def get_selection_rectangle (self):
        y_start = self.__top_legend.get_height () + self.__data.get_height () + self.__mid_scale.get_height () + 20
        y_height = self.__bot_scale.get_height () + 20
        x_start = self.__bot_scale.get_position (self.__r_start)
        x_end = self.__bot_scale.get_position (self.__r_end)
        return (x_start,y_start,x_end-x_start,y_height)
    def scale_selection (self, x):
        x_scaled = x / self.__width * (self.__end - self.__start)
        return x_scaled
    def set_range (self,start, end):
        s = min (start, end)
        e = max (start, end)
        start = max (self.__start, s)
        end = min (self.__end, e)
        self.__r_start = start
        self.__r_end = end
        self.__data.set_render_range (start, end)
        self.__mid_scale.set_bounds (start, end)
        self.layout (self.__width, self.__height)
    def get_range (self):
        return (self.__r_start, self.__r_end)
    def set_data (self, data):
        self.__data = data
    def set_top_legend (self, top_legend):
        self.__top_legend = top_legend
    def layout (self, width, height):
        self.__width = width
        self.__height = height
        self.__top_legend.layout (width)
        top_legend_height = self.__top_legend.get_height ()
        self.__data.layout (width)
        self.__mid_scale.layout (width - self.__data.get_data_x_start ())
        self.__bot_scale.layout (width)
        return
    def __x_pixel (self, x, width):
        new_x = (x - self.__start) * width / (self.__end - self.__start)
        return new_x
    
    def draw (self, ctx):
        # default background is white
        ctx.save ()
        ctx.set_source_rgb (1, 1, 1)
        ctx.set_operator (cairo.OPERATOR_SOURCE)
        ctx.rectangle (0,0,self.__width,self.__height)
        ctx.fill ()

        # top legend 
        ctx.save ()
        self.__top_legend.draw (ctx)
        top_legend_height = self.__top_legend.get_height ()
        ctx.restore ()

        # separation line
        ctx.move_to (0, top_legend_height)
        ctx.line_to (self.__width, top_legend_height)
        ctx.close_path ()
        ctx.set_line_width (2)
        ctx.set_source_rgb (0,0,0)
        ctx.stroke ()

        # data
        ctx.save ()
        ctx.translate (0,
                       top_legend_height)
        self.__data.draw (ctx)
        ctx.restore ()

        # scale below data
        ctx.save ()
        ctx.translate (self.__data.get_data_x_start (),
                       top_legend_height + self.__data.get_height () + self.__mid_scale.get_height ())
        self.__mid_scale.draw (ctx)
        ctx.restore ()

        height_used = top_legend_height + self.__data.get_height () + self.__mid_scale.get_height ()

        # separation between scale and left pane
        ctx.move_to (self.__data.get_data_x_start (), height_used)
        ctx.rel_line_to (0, -self.__mid_scale.get_height ())
        ctx.close_path ()
        ctx.set_source_rgb (0,0,0)
        ctx.set_line_width (2)
        ctx.stroke ()

        # separation below scale
        ctx.move_to (0, height_used)
        ctx.line_to (self.__width, height_used)
        ctx.close_path ()
        ctx.set_line_width (2)
        ctx.set_source_rgb (0,0,0)
        ctx.stroke ()

        select_start = self.__bot_scale.get_position (self.__r_start)
        select_end = self.__bot_scale.get_position (self.__r_end)

        # left connection between top scale and bottom scale
        ctx.move_to (0, height_used);
        ctx.line_to (self.__data.get_data_x_start (), height_used)
        ctx.line_to (select_start, height_used + 20)
        ctx.line_to (0, height_used + 20)
        ctx.line_to (0,height_used)
        ctx.set_source_rgb (0,0,0)
        ctx.set_line_width (1)
        ctx.stroke_preserve ()
        ctx.set_source_rgb (0.9,0.9,0.9)
        ctx.fill ()

        # right connection between top scale and bottom scale
        ctx.move_to (self.__width, height_used)
        ctx.line_to (self.__width, height_used+20)
        ctx.line_to (select_end, height_used+20)
        ctx.line_to (self.__width, height_used)
        ctx.set_source_rgb (0,0,0)
        ctx.set_line_width (1)
        ctx.stroke_preserve ()
        ctx.set_source_rgb (0.9,0.9,0.9)
        ctx.fill ()

        height_used += 20

        # unused area background
        unused_start = self.__bot_scale.get_position (self.__r_start)
        unused_end = self.__bot_scale.get_position (self.__r_end)
        unused_height = self.__bot_scale.get_height () + 20
        ctx.rectangle (0, height_used,
                       unused_start,
                       unused_height)
        ctx.rectangle (unused_end,
                       height_used,
                       self.__width - unused_end,
                       unused_height)
        ctx.set_source_rgb (0.9,0.9,0.9)
        ctx.fill ()        

        # border line around bottom scale
        ctx.move_to (unused_end, height_used)
        ctx.line_to (self.__width, height_used)
        ctx.line_to (self.__width, height_used + unused_height)
        ctx.line_to (0, height_used + unused_height)
        ctx.line_to (0, height_used)
        ctx.line_to (unused_start, height_used)
        ctx.close_path ()
        ctx.set_line_width (2)
        ctx.set_source_rgb (0,0,0)
        ctx.stroke ()
        ctx.move_to (unused_start, height_used)
        ctx.line_to (unused_end, height_used)
        ctx.close_path ()
        ctx.set_line_width (1)
        ctx.set_source_rgb (0.9,0.9,0.9)
        ctx.stroke ()

        # unused area dot borders
        ctx.save ()
        ctx.move_to (max (unused_start, 2), height_used)
        ctx.rel_line_to (0,unused_height)
        ctx.move_to (min (unused_end, self.__width-2), height_used)
        ctx.rel_line_to (0, unused_height)
        ctx.set_dash ([5], 0)
        ctx.set_source_rgb (0,0,0)
        ctx.set_line_width (1)
        ctx.stroke ()
        ctx.restore ()

        # bottom scale
        ctx.save ()
        ctx.translate (0, height_used)
        self.__bot_scale.draw (ctx)
        ctx.restore ()

class GtkGraphicRenderer (gtk.DrawingArea):
    def __init__ (self, data):
        super (GtkGraphicRenderer, self).__init__ ()
        self.__data = data
        self.__moving_left = False
        self.__moving_right = False
        self.__moving_both = False
        self.__moving_top = False
        self.__force_full_redraw = True
        self.add_events (gtk.gdk.POINTER_MOTION_MASK)
        self.add_events (gtk.gdk.BUTTON_PRESS_MASK)
        self.add_events (gtk.gdk.BUTTON_RELEASE_MASK)
        self.connect ("expose_event", self.expose)
        self.connect ('size-allocate', self.size_allocate)
        self.connect ('motion-notify-event', self.motion_notify)
        self.connect ('button-press-event', self.button_press)
        self.connect ('button-release-event', self.button_release)
    def set_smaller_zoom (self):
        (start, end) = self.__data.get_range ()
        self.__data.set_range (start, start+(end-start)*2)
        self.__force_full_redraw = True
        self.queue_draw ()
    def set_bigger_zoom (self):
        (start, end) = self.__data.get_range ()
        self.__data.set_range (start, start+(end-start)/2)
        self.__force_full_redraw = True
        self.queue_draw ()
    def output_png (self, filename):
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                     self.__data.get_width (),
                                     self.__data.get_height ())
        ctx = cairo.Context (self.__buffer_surface)
        self.__data.draw (ctx)
        surface.write_to_png (filename)
    def button_press (self, widget, event):
        (x, y, width, height) = self.__data.get_selection_rectangle ()
        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle ()
        if event.y > y and event.y < y+height:
            if abs (event.x - x) < 5:
                self.__moving_left = True
                return True
            if abs (event.x - (x+width)) < 5:
                self.__moving_right = True
                return True
            if event.x > x and event.x < x+width:
                self.__moving_both = True
                self.__moving_both_start = event.x
                self.__moving_both_cur = event.x
                return True
        if event.y > d_y and event.y < (d_y + d_height):
            if event.x > d_x and event.x < (d_x + d_width):
                self.__moving_top = True
                self.__moving_top_start = event.x
                self.__moving_top_cur = event.x
                return True
        return False
    def button_release (self, widget, event):
        if self.__moving_left:
            self.__moving_left = False
            left = self.__data.scale_selection (self.__moving_left_cur)
            right = self.__data.get_range ()[1]
            self.__data.set_range (left, right)
            self.__force_full_redraw = True
            self.queue_draw ()
            return True
        if self.__moving_right:
            self.__moving_right = False
            right = self.__data.scale_selection (self.__moving_right_cur)
            left = self.__data.get_range ()[0]
            self.__data.set_range (left, right)
            self.__force_full_redraw = True
            self.queue_draw ()
            return True
        if self.__moving_both:
            self.__moving_both = False
            delta = self.__data.scale_selection (self.__moving_both_cur - self.__moving_both_start)
            (left, right) = self.__data.get_range ()
            self.__data.set_range (left+delta, right+delta)
            self.__force_full_redraw = True
            self.queue_draw ()
            return True
        if self.__moving_top:
            self.__moving_top = False
        return False
    def motion_notify (self, widget, event):
        (x, y, width, height) = self.__data.get_selection_rectangle ()
        if self.__moving_left:
            if event.x <= 0:
                self.__moving_left_cur = 0
            elif event.x >= x+width:
                self.__moving_left_cur = x+width
            else:
                self.__moving_left_cur = event.x
            self.queue_draw_area (0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_right:
            if event.x >= self.__width:
                self.__moving_right = self.__width
            elif event.x < x:
                self.__moving_right_cur = x
            else:
                self.__moving_right_cur = event.x
            self.queue_draw_area (0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_both:
            cur_e = self.__width - (x + width - self.__moving_both_start)
            cur_s = (self.__moving_both_start - x)
            if event.x < cur_s:
                self.__moving_both_cur = cur_s
            elif event.x > cur_e:
                self.__moving_both_cur = cur_e
            else:
                self.__moving_both_cur = event.x
            self.queue_draw_area (0, int(y), int(self.__width), int(height))
            return True
        if self.__moving_top:
            self.__moving_top_cur = event.x
            delta = self.__data.scale_data (self.__moving_top_start-self.__moving_top_cur)
            (left, right) = self.__data.get_range ()
            self.__data.set_range (left+delta, right+delta)
            self.__force_full_redraw = True
            self.__moving_top_start = event.x
            self.queue_draw ()
            return True
        (d_x, d_y, d_width, d_height) = self.__data.get_data_rectangle ()
        if event.y > y and event.y < y+height:
            if abs (event.x - x) < 5 or abs (event.x - (x+width)) < 5:
                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.SB_H_DOUBLE_ARROW))
                return True
            if event.x > x and event.x < x+width:
                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.FLEUR))
                return True
        if event.y > d_y and event.y < (d_y + d_height):
            if event.x > d_x and event.x < (d_x + d_width):
                widget.window.set_cursor (gtk.gdk.Cursor (gtk.gdk.FLEUR))
                return True
        widget.window.set_cursor (None)
        return False
    def size_allocate (self, widget, allocation):
        self.__width = allocation.width
        self.__height = allocation.height
        self.__data.layout (allocation.width, allocation.height)
        self.__force_full_redraw = True
        self.queue_draw ()
    def expose (self, widget, event):
        if self.__force_full_redraw:
            self.__buffer_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                                       self.__data.get_width (),
                                                       self.__data.get_height ())
            ctx = cairo.Context(self.__buffer_surface)
            self.__data.draw (ctx)
            self.__force_full_redraw = False
        ctx = widget.window.cairo_create()
        ctx.rectangle(event.area.x, event.area.y,
                      event.area.width, event.area.height)
        ctx.clip()
        ctx.set_source_surface (self.__buffer_surface)
        ctx.paint ()
        (x, y, width, height) = self.__data.get_selection_rectangle ()
        if self.__moving_left:
            ctx.move_to (max (self.__moving_left_cur, 2), y)
            ctx.rel_line_to (0, height)
            ctx.close_path ()
            ctx.set_line_width (1)
            ctx.set_source_rgb (0,0,0)
            ctx.stroke ()
        if self.__moving_right:
            ctx.move_to (min (self.__moving_right_cur, self.__width-2), y)
            ctx.rel_line_to (0, height)
            ctx.close_path ()
            ctx.set_line_width (1)
            ctx.set_source_rgb (0,0,0)
            ctx.stroke ()
        if self.__moving_both:
            delta_x = self.__moving_both_cur - self.__moving_both_start
            left_x = x + delta_x
            ctx.move_to (x+delta_x, y)
            ctx.rel_line_to (0, height)
            ctx.close_path ()
            ctx.move_to (x+width+delta_x, y)
            ctx.rel_line_to (0, height)
            ctx.close_path ()
            ctx.set_source_rgb (0,0,0)
            ctx.set_line_width (1)
            ctx.stroke ()
        return False

class MainWindow:
    def __init__ (self):
        return
    def run (self, graphic):
        window = gtk.Window()
        self.__window = window
        window.set_default_size (200, 200)
        vbox = gtk.VBox ()
        window.add (vbox)
        render = GtkGraphicRenderer(graphic)
        self.__render = render
        vbox.pack_end (render, True, True, 0)
        hbox = gtk.HBox ()
        vbox.pack_start (hbox, False, False, 0)
        smaller_zoom = gtk.Button ("Zoom Out")
        smaller_zoom.connect ("clicked", self.__set_smaller_cb)
        hbox.pack_start (smaller_zoom)
        bigger_zoom = gtk.Button ("Zoom In")
        bigger_zoom.connect ("clicked", self.__set_bigger_cb)
        hbox.pack_start (bigger_zoom)
        output_png = gtk.Button ("Output Png")
        output_png.connect ("clicked", self.__output_png_cb)
        hbox.pack_start (output_png)
        window.connect('destroy', gtk.main_quit)
        window.show_all()
        #gtk.bindings_activate (gtk.main_quit, 'q', 0)
        gtk.main()
    def __set_smaller_cb (self, widget):
        self.__render.set_smaller_zoom ()
    def __set_bigger_cb (self, widget):
        self.__render.set_bigger_zoom ()
    def __output_png_cb (self, widget):
        dialog = gtk.FileChooserDialog ("Output Png", self.__window,
                                        gtk.FILE_CHOOSER_ACTION_SAVE, ("Save",1))
        self.__dialog = dialog
        dialog.set_default_response (1)
        dialog.connect ("response", self.__dialog_response_cb)
        dialog.show ()
        return
    def __dialog_response_cb (self, widget, response):
        if response == 1:
            filename = self.__dialog.get_filename ()
            self.__render.output_png (filename)
            widget.hide ()
        return



def read_data(filename):
    timelines = Timelines ()
    colors = Colors ()
    fh = open(filename)
    m1 = re.compile ('range ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
    m2 = re.compile ('event-str ([^ ]+) ([^ ]+) ([^ ]+) ([0-9]+)')
    m3 = re.compile ('event-int ([^ ]+) ([^ ]+) ([0-9]+) ([0-9]+)')
    m4 = re.compile ('color ([^ ]+) #([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})([a-fA-F0-9]{2,2})')
    for line in fh.readlines():
        m = m1.match (line)
        if m:
            line_name = m.group (1)
            timeline = timelines.get (m.group (1))
            rang = timeline.get_range (m.group (2))
            data_range = DataRange ()
            data_range.value = m.group (3)
            data_range.start = int (m.group (4))
            data_range.end = int (m.group (5))
            rang.add_range (data_range)
            continue
        m = m2.match (line)
        if m:
            line_name = m.group (1)
            timeline = timelines.get (m.group (1))
            ev = timeline.get_event_str (m.group (2))
            event = EventString ()
            event.value = m.group (3)
            event.at = int (m.group (4))
            ev.add_event (event)
            continue
        m = m3.match (line)
        if m:
            line_name = m.group (1)
            timeline = timelines.get (m.group (1))
            ev = timeline.get_event_int (m.group (2))
            event = EventInt ()
            event.value = int (m.group (3))
            event.at = int (m.group (4))
            ev.add_event (event)
            continue
        
        m = m4.match (line)
        if m:
            r = int (m.group (2), 16)
            g = int (m.group (3), 16)
            b = int (m.group (4), 16)
            color = Color (r/255, g/255, b/255)
            colors.add (m.group (1), color)
            continue
    timelines.sort ()
    return (colors, timelines)



def main():
    (colors, timelines) = read_data (sys.argv[1])
    (lower_bound, upper_bound) = timelines.get_bounds ()
    graphic = GraphicRenderer (lower_bound, upper_bound)
    top_legend = TopLegendRenderer ()
    range_values = timelines.get_all_range_values ()
    range_colors = []
    for range_value in range_values:
        range_colors.append (colors.lookup (range_value))
    top_legend.set_legends (range_values,
                            range_colors)
    graphic.set_top_legend (top_legend)
    data = TimelinesRenderer ()
    data.set_timelines (timelines, colors)
    graphic.set_data (data)

    # default range
    range_mid = (upper_bound - lower_bound) /2
    range_width = (upper_bound - lower_bound) /10
    range_lo = range_mid - range_width / 2
    range_hi = range_mid + range_width / 2
    graphic.set_range (range_lo, range_hi)

    main_window = MainWindow ()
    main_window.run (graphic)


main ()