Ejemplo n.º 1
0
class DrawWithStyle(HasSignals):
    def __init__(self, graph, data):

        self.graph, self.data = graph, data

        self.style = Style()

        try:
            c = self.data.color
            self.style.color = (c % 256, (c // 256) % 256,
                                (c // (256 * 256)) % 256)
        except ValueError:
            self.style.color = default_style.color
            self.data.color = '0'

        if self.data.size == 0:
            self.data.size = 6
        self.style.symbol_size = self.data.size

        if self.data.symbol == '':
            self.data.symbol = 'square-f'
        self.style.symbol = self.data.symbol

        if self.data.linestyle == '':
            self.data.linestyle = 'solid'
        self.style.line_style = self.data.linestyle

        if self.data.linetype == '':
            self.data.linetype = 'none'
        self.style.line_type = self.data.linetype

        self.style.line_width = self.data.linewidth

        self.style.connect('modified', self.on_style_modified)

        self.xfrom, self.xto = -inf, inf

    def change_style_do(self, item, value, old):
        if item == 'color':
            self.data.color = (self.style.color[0] +
                               self.style.color[1] * 256 +
                               self.style.color[2] * 256 * 256)
        elif item == 'symbol':
            self.data.symbol = self.style.symbol
        elif item == 'symbol_size':
            self.data.size = self.style.symbol_size
        elif item == 'line_type':
            self.data.linetype = self.style.line_type
        elif item == 'line_style':
            self.data.linestyle = self.style.line_style
        elif item == 'line_width':
            self.data.linewidth = self.style.line_width

        return [item, value, old]

    def change_style_redo(self, state):
        item, value, old = state
        setattr(self.style, item, value)

    def change_style_undo(self, state):
        item, value, old = state
        setattr(self.style, item, old)

    def change_style_combine(self, state, other):
        #        print state, other
        return False

    change_style = action_from_methods('dataset-change-style',
                                       change_style_do,
                                       change_style_undo,
                                       change_style_redo,
                                       combine=change_style_combine)

    def on_style_modified(self, item, value, old):
        self.change_style(item, value, old)
        self.emit('modified', self)

    def paint_symbols(self, x, y):
        if self.style.symbol != 'none' and self.style.symbol_size != 0:
            glColor4f(self.style.color[0] / 256., self.style.color[1] / 256.,
                      self.style.color[2] / 256., 1.)
            gl2ps_PointSize(self.data.size)
            if self.data.size != 0:
                glPointSize(self.data.size)

            xmin = max(self.graph.xmin, self.xfrom)
            xmax = min(self.graph.xmax, self.xto)
            xmin, ymin = self.graph.data_to_phys(xmin, self.graph.ymin)
            xmax, ymax = self.graph.data_to_phys(xmax, self.graph.ymax)

            symbols = [
                'circle', 'square', 'diamond', 'uptriangle', 'downtriangle',
                'lefttriangle', 'righttriangle'
            ]

            render_symbols(x, y, symbols.index(self.style.symbol[:-2]),
                           ['o', 'f'].index(self.style.symbol[-1]),
                           self.style.symbol_size, xmin, xmax, ymin, ymax,
                           self.graph.plot_width, self.graph.plot_height,
                           self.graph.xmin, self.graph.xmax, self.graph.ymin,
                           self.graph.ymax, self.graph.xtype == 'log',
                           self.graph.ytype == 'log')

    def paint_lines(self, x, y):
        if len(x) == 0:
            return
        glColor4f(self.style.color[0] / 256., self.style.color[1] / 256.,
                  self.style.color[2] / 256., 1.)

        #        x = array([xi for (xi, yi) in zip(xx, yy) if xi is not nan and yi is not nan])
        #        y = array([yi for (xi, yi) in zip(xx, yy) if xi is not nan and yi is not nan])
        #        x, y = self.graph.proj(x, y)
        if self.style.line_style == 'dotted':
            glLineStipple(1, 0x4444)
            glEnable(GL_LINE_STIPPLE)
        elif self.style.line_style == 'dashed':
            glLineStipple(3, 0x4444)
            glEnable(GL_LINE_STIPPLE)
        elif self.style.line_style == 'solid':
            glDisable(GL_LINE_STIPPLE)

        if self.style.line_type == 'bspline':
            z = zeros(len(x))
            N = len(x)
            nurb = gluNewNurbsRenderer()
            gluNurbsProperty(nurb, GLU_AUTO_LOAD_MATRIX, GL_TRUE)
            gluNurbsProperty(nurb, GLU_SAMPLING_TOLERANCE, 5)
            gluBeginCurve(nurb)
            x, y = self.graph.data_to_phys(x, y)
            gluNurbsCurve(nurb, arange(3 + N), transpose(array([x, y, z])),
                          GL_MAP1_VERTEX_3)
            gluEndCurve(nurb)
        elif self.style.line_type == 'straight':
            xmin = max(self.graph.xmin, self.xfrom)
            xmax = min(self.graph.xmax, self.xto)
            xmin, ymin = self.graph.data_to_phys(xmin, self.graph.ymin)
            xmax, ymax = self.graph.data_to_phys(xmax, self.graph.ymax)
            render_lines(x, y, xmin, xmax, ymin, ymax, self.graph.plot_width,
                         self.graph.plot_height, self.graph.xmin,
                         self.graph.xmax, self.graph.ymin, self.graph.ymax,
                         self.graph.xtype == 'log', self.graph.ytype == 'log')


#            glVertexPointerd(transpose(array([x, y, z])).tostring())
#            glEnable(GL_VERTEX_ARRAY)
#            glDrawArrays(GL_LINE_STRIP, 0, N)
#            glDisable(GL_VERTEX_ARRAY)

        glDisable(GL_LINE_STIPPLE)
Ejemplo n.º 2
0
class Project(HasSignals):
    def __init__(self, filename=None):
        if isinstance(filename, unicode):
            filename = filename.encode(sys.getfilesystemencoding())
        self.filename = filename

        if self.filename is None:
            # We initially create an in-memory database.
            # When we save to a file, we will reopen the database from the file.
            self.db = metakit.storage()
#            self.filename = 'defau.gt'
#            self.db = metakit.storage(self.filename, 1)
#            for desc in storage_desc.values():
#                self.db.getas(desc)
#            self.db.commit()
        else:
            self.db = metakit.storage(self.filename, 1)
            self.cleanup()

#        self.aside = metakit.storage('grafit-storage.mka', 1)
#        self.db.aside(self.aside)
#        print >>sys.stderr, "project created"

        self._modified = False

        action_list.connect('added', self.on_action_added)

        self.items = {}
        self.deleted = {}
        self._dict = {}
        self.save_dict = {}

        # Create top folder.
        # - it must be created before all other items
        # - it must be created with _isroot=True, to set itself as its parent folder
        try:
            fv = self.db.getas(storage_desc[Folder])
            row = fv.select(name='top')[0]
            self.top = self.items[row.id] = Folder(self,
                                                   location=(fv, row, row.id),
                                                   _isroot=True)
        except IndexError:
            # can't find it in the database, create a new one.
            self.top = Folder(self, 'top', _isroot=True)

        self.here = self.top
        self.this = None

        # create objects
        for cls, desc in [(i, storage_desc[i])
                          for i in (Folder, grafit.worksheet.Worksheet,
                                    grafit.graph.Graph)]:
            view = self.db.getas(desc)
            for i, row in enumerate(view):
                if row.id != self.top.id:
                    if not row.id.startswith('-'):
                        #                        print 'loading', cls, row.id,
                        self.items[row.id] = cls(self,
                                                 location=(view, row, row.id))
#                        print 'end'
                    else:
                        self.deleted[row.id] = cls(self,
                                                   location=(view, row,
                                                             row.id))

    def on_action_added(self, action=None):
        self.modified = True

    def cd(self, folder):
        # restore dictionary
        for o in self.here.contents():
            try:
                del self._dict[o.name]
            except KeyError:
                pass
        self._dict.update(self.save_dict)
        self._save_dict = {}

        self.here = folder

        # update dictionary
        self._dict['here'] = self.here
        self._dict['up'] = self.here.up
        for o in self.here.contents():
            if o.name in self._dict:
                self._save_dict[o.name] = self._dict[o.name]
            self._dict[o.name] = o

        self.emit('change-current-folder', folder)

    def set_current(self, obj):
        if obj not in list(self.here.contents()):
            raise NotImplementedError
        self.this = obj
        self._dict['this'] = self.this
        self.emit('set-current-object', obj)

    def set_dict(self, d):
        self._dict = d

        self._dict['top'] = self.top
        self._dict['this'] = self.this
        self.cd(self.here)

    def unset_dict(self):
        for o in self.here.contents():
            if o.name in self._dict:
                self._save_dict[o.name] = self._dict[o.name]
            self._dict[o.name] = o

    def cleanup(self):
        """Purge all deleted items from the database"""
        for cls, desc in storage_desc.iteritems():
            view = self.db.getas(desc)
            for i, row in enumerate(view):
                if row.id.startswith('-'):
                    view.delete(i)

    def _create(self, cls, location):
        """Create a new entry a new item of class `cls` in the database

        This method is called from the constructor of all `Item`-derived
        classes, if the item is not already in the database.
        Returns the view, row and id of the new item.
        """
        try:
            view = self.db.getas(storage_desc[cls])
        except KeyError:
            raise TypeError, "project cannot create an item of type '%s'" % cls

        id = create_id()
        from mk import addrow
        if location is None:
            row = view.append(id=id)
        else:
            row = addrow(view, location)
            view[row].id = id
        data = view[row]

        return view, data, id

    # new ##################################

    def new(self, cls, *args, **kwds):
        obj = cls(self, *args, **kwds)
        self.items[obj.id] = obj
        if obj.parent is self.top:
            self._dict[obj.name] = obj
        # don't emit 'add-item' because it is emitted by Item.__init__
        return obj, obj

    def new_undo(self, obj):
        del self.items[obj.id]
        obj.id = '-' + obj.id
        self.deleted[obj.id] = obj
        if obj.parent is self.top and obj.name in self._dict:
            del self._dict[obj.name]
        self.emit('remove-item', obj)
        obj.parent.emit('modified')

    def new_redo(self, obj):
        del self.deleted[obj.id]
        obj.id = obj.id[1:]
        self.items[obj.id] = obj
        if obj.parent is self.top:
            self._dict[obj.name] = obj
        self.emit('add-item', obj)
        obj.parent.emit('modified')

    def new_cleanup(self, obj):
        if obj.id in self.deleted:
            del self.deleted[obj.id]
        obj.view.remove(obj.view.select(id=obj.id))

    new = action_from_methods('project_new', new, new_undo, new_redo,
                              new_cleanup)

    # remove ###############################

    def remove(self, id):
        obj = self.items[id]
        ind = obj.view.find(id=id)

        if obj.name in self._dict and obj.name in self._dict:
            del self._dict[obj.name]

        if ind == -1:
            raise NameError
        else:
            del self.items[id]
            obj.id = '-' + obj.id
            self.deleted[obj.id] = obj

        self.emit('remove-item', obj)
        return id

    def remove_undo(self, id):
        obj = self.deleted['-' + id]
        ind = obj.view.find(id=obj.id)

        del self.deleted[obj.id]
        obj.id = obj.id[1:]
        self.items[obj.id] = obj

        if obj.parent is self.top:
            self._dict[obj.name] = obj
        self.emit('add-item', obj)

    remove = action_from_methods('project_remove', remove, remove_undo)

    # Shortcuts for creating and removing folders

    def mkfolder(self, path, parent=None):
        self.new(Folder, path, parent)

    def rmfolder(self, path):
        if path in self.here:
            self.remove(self.here[path].id)
        else:
            raise NameError, "folder '%s' does not exist" % path

#    def icommit(self):
#        print >>sys.stderr, 'icommit'
#        self.db.commit()
#        self.aside.commit()

    def commit(self):
        #        self.db.commit(1)
        self.db.commit()
        #        self.aside.commit()
        self.modified = False

    def saveto(self, filename):
        if isinstance(filename, unicode):
            filename = filename.encode(sys.getfilesystemencoding())
        try:
            f = open(filename, 'wb')
            self.db.save(f)
        finally:
            f.close()
            self.modified = False

    def get_modified(self):
        return self._modified

    def set_modified(self, value):
        #        if value:
        #            self.icommit()
        if value and not self._modified:
            self.emit('modified')
        elif self._modified and not value:
            self.emit('not-modified')
        self._modified = value

    modified = property(get_modified, set_modified)
Ejemplo n.º 3
0
    def __setitem__(self, key, value):
        prev = self[key]
        MkArray.__setitem__(self, key, value)
        self.worksheet.emit('data-changed')
        self.emit('data-changed')
        return [key, value, prev]

    def undo_setitem(self, state):
        key, value, prev = state
        self[key] = prev

    def __eq__(self, other):
        return self.id == other.id

    __setitem__ = action_from_methods('column_change_data', __setitem__, undo_setitem)


class Worksheet(Item, HasSignals):
    def __init__(self, project, name=None, parent=None, location=None):
        self.__attr = False

        Item.__init__(self, project, name, parent, location)

        self.columns = []

        if location is not None:
            for i in range(len(self.data.columns)):
                if not self.data.columns[i].name.startswith('-'):
                    self.columns.append(Column(self, i))
Ejemplo n.º 4
0
    def __setitem__(self, key, value):
        prev = self[key]
        MkArray.__setitem__(self, key, value)
        self.worksheet.emit('data-changed')
        self.emit('data-changed')
        return [key, value, prev]

    def undo_setitem(self, state):
        key, value, prev = state
        self[key] = prev

    def __eq__(self, other):
        return self.id == other.id

    __setitem__ = action_from_methods('column_change_data', __setitem__, undo_setitem)


class Worksheet(Item, HasSignals):
    def __init__(self, project, name=None, parent=None, location=None):
        self.__attr = False

        Item.__init__(self, project, name, parent, location)

        self.columns = []

        if location is not None:
            for i in range(len(self.data.columns)):
                if not self.data.columns[i].name.startswith('-'):
                    self.columns.append(Column(self, i))
Ejemplo n.º 5
0
class FunctionInstance(HasSignals):
    def __init__(self, function, name):
        self.name = name
        self.function = function
        self.callable = self.function.to_module()
        self._parameters = [1.] * len(function.parameters)
        self.reg = True
        self._old = None
        self.__old = None

    def update(self):
        self.emit('modified')

    def move(self, x, y):
        if not hasattr(self.function, 'move'):
            return
        self.parameters = self.function.move(x, y, *self.parameters)
        self.emit('modified')

    def __call__(self, arg):
        try:
            return self.callable(arg, *self.parameters)
        except (ValueError, OverflowError):
            # If we don't catch these errors here,
            # odr segfaults on us!
            if hasattr(arg, '__len__'):
                return array([nan] * len(arg))
            else:
                return nan

    def set_reg(self, on):
        if self.reg == on:
            return
        elif on:
            self._old = self.__old
        else:
            self.__old = self._parameters
        self.reg = on

    def set_parameters(self, p):
        #        print >>sys.stderr, self, self._parameters, p
        if self._old is not None:
            old = self._old
            self._old = None
        else:
            old = self._parameters
        self._parameters = p
        if not self.reg:
            raise StopAction
        if old == p:
            # if the values haven't changed, don't bother
            raise StopAction
        return [old, p]

    def get_parameters(self):
        return self._parameters

    def redo_set_parameters(self, state):
        old, p = state
        self._parameters = p
        self.emit('modified')

    def undo_set_parameters(self, state):
        old, p = state
        self._parameters = old
        self.emit('modified')

    def combine_set_parameters(self, state, other):
        #        print state, other
        #        print 'attempt to combine', state, other
        return False

    set_parameters = action_from_methods('function-change-parameters',
                                         set_parameters,
                                         undo_set_parameters,
                                         redo_set_parameters,
                                         combine=combine_set_parameters)
    parameters = property(get_parameters, set_parameters)
Ejemplo n.º 6
0
class Graph(Item, HasSignals):
    def __init__(self, project, name=None, parent=None, location=None):
        Item.__init__(self, project, name, parent, location)

        self.paint_xor_objects = False
        self.selected_datasets = []

        self.mode = 'arrow'

        self.graph_objects = []
        self.dragobj = None

        self.selected_object = None

        self.plot_height = 100
        self.plot_width = 100

        self.datasets = []
        if location is not None:
            for i, l in enumerate(self.data.datasets):
                if not l.id.startswith('-'):
                    self.datasets.append(Dataset(self, i))
                    self.datasets[-1].connect('modified',
                                              self.on_dataset_modified)
            for l in self.data.lines:
                if not l.id.startswith('-'):
                    self.graph_objects.append(Line(self, l))
            for l in self.data.text:
                if not l.id.startswith('-'):
                    self.graph_objects.append(Text(self, l))

        self.functions = []
        #        if location is not None:
        #            for i in range(len(self.data.functions)):
        #                if not self.data.functions[i].id.startswith('-'):
        #                    f = Function(self, i)
        #                    self.functions.append(f)
        #                    f.connect('modified', self.on_dataset_modified)
        #                    f.func.connect('modified', self.on_dataset_modified)

        self.ps = False

        self.axis_top = Axis('top', self)
        self.axis_bottom = Axis('bottom', self)
        self.axis_right = Axis('right', self)
        self.axis_left = Axis('left', self)

        self.axes = [
            self.axis_top, self.axis_right, self.axis_bottom, self.axis_left
        ]

        self.grid_h = Grid('horizontal', self)
        self.grid_v = Grid('vertical', self)

        self.set_range(0.0, 100.5)
        if location is None:
            self.xmin, self.ymin = 0, 0
            self.ymax, self.xmax = 10, 10
        self.newf()

        if self.xtype == '':
            self._xtype = 'linear'
        if self.ytype == '':
            self._ytype = 'linear'
        self.selected_function = None

        self.rubberband = Rubberband(self)
        self.cross = Cross(self)
        self.rangehandle = Rangehandle(self)

        self.objects = [self.rubberband, self.cross, self.rangehandle]
        self.textpainter = TextPainter(self)

        self.axis_title_font_size = 12.
        self.background_color = (1., 1., 1., 1.)
        self.pwidth = 120.
        self.pheight = 100.

        self.recalc = True

    default_name_prefix = 'graph'

    def redraw(self, recalc=False):
        if recalc:
            self.recalc = True
        self.emit('redraw')

    def get_xmin(self):
        try:
            return float(self._zoom.split()[0])
        except IndexError:
            return 0.0

    def get_xmax(self):
        try:
            return float(self._zoom.split()[1])
        except IndexError:
            return 1.0

    def get_ymin(self):
        try:
            return float(self._zoom.split()[2])
        except IndexError:
            return 0.0

    def get_ymax(self):
        try:
            return float(self._zoom.split()[3])
        except IndexError:
            return 1.0

    def set_xmin(self, value):
        self._zoom = ' '.join(
            [str(f) for f in [value, self.xmax, self.ymin, self.ymax]])

    def set_xmax(self, value):
        self._zoom = ' '.join(
            [str(f) for f in [self.xmin, value, self.ymin, self.ymax]])

    def set_ymin(self, value):
        self._zoom = ' '.join(
            [str(f) for f in [self.xmin, self.xmax, value, self.ymax]])

    def set_ymax(self, value):
        self._zoom = ' '.join(
            [str(f) for f in [self.xmin, self.xmax, self.ymin, value]])

    xmin = property(get_xmin, set_xmin)
    xmax = property(get_xmax, set_xmax)
    ymin = property(get_ymin, set_ymin)
    ymax = property(get_ymax, set_ymax)

    # axis scales

    def set_xtype(self, _state, tp):
        if tp == 'log' and (self.xmin <= 0 or self.xmax <= 0):
            raise StopAction
        _state['old'] = self._xtype
        self._xtype = tp
        self.redraw(True)

    def undo_set_xtype(self, _state):
        self._xtype = _state['old']
        self.redraw(True)

    set_xtype = action_from_methods2('graph-set-xaxis-scale', set_xtype,
                                     undo_set_xtype)

    def get_xtype(self):
        return self._xtype

    xtype = property(get_xtype, set_xtype)

    def set_ytype(self, _state, tp):
        if tp == 'log' and (self.xmin <= 0 or self.xmax <= 0):
            raise StopAction
        _state['old'] = self._ytype
        self._ytype = tp
        self.redraw(True)

    def undo_set_ytype(self, _state):
        self._ytype = _state['old']
        self.redraw(True)

    set_ytype = action_from_methods2('graph-set-xaxis-scale', set_ytype,
                                     undo_set_ytype)

    def get_ytype(self):
        return self._ytype

    ytype = property(get_ytype, set_ytype)

    # titles

    def set_xtitle(self, state, title):
        state['old'], state['new'] = self._xtitle, title
        self._xtitle = title
        self.reshape()
        self.redraw()

    def undo_set_xtitle(self, state):
        self._xtitle = state['old']
        self.reshape()
        self.redraw()

    def redo_set_xtitle(self, state):
        self._xtitle = state['new']
        self.reshape()
        self.redraw()

    def get_xtitle(self):
        return self._xtitle

    set_xtitle = action_from_methods2('graph/set-xtitle',
                                      set_xtitle,
                                      undo_set_xtitle,
                                      redo=redo_set_xtitle)
    xtitle = property(get_xtitle, set_xtitle)

    def set_ytitle(self, state, title):
        state['old'], state['new'] = self._ytitle, title
        self._ytitle = title
        self.reshape()
        self.redraw()

    def undo_set_ytitle(self, state):
        self._ytitle = state['old']
        self.reshape()
        self.redraw()

    def redo_set_ytitle(self, state):
        self._ytitle = state['new']
        self.reshape()
        self.redraw()

    def get_ytitle(self):
        return self._ytitle

    set_ytitle = action_from_methods2('graph/set-ytitle',
                                      set_ytitle,
                                      undo_set_ytitle,
                                      redo=redo_set_ytitle)
    ytitle = property(get_ytitle, set_ytitle)

    def __repr__(self):
        return '<Graph %s%s>' % (self.name,
                                 '(deleted)' * self.id.startswith('-'))

    def newf(self):
        #        ind = self.data.functions.append(id=create_id())
        f = Function(self)
        f.connect('modified', self.on_dataset_modified)
        f.func.connect('modified', self.on_dataset_modified)
        f.func.connect('add-term', self.on_dataset_modified)
        f.func.connect('remove-term', self.on_dataset_modified)
        self.functions.append(f)
        self.emit('add-function', f)
        return f

    def create_legend(self):
        legend = self.new_object(Text)
        legend.text = '\n'.join('@%d@' % i + str(d)
                                for i, d in enumerate(self.datasets))

    # add and remove graph objects
    def new_object(self, state, typ):
        location = {Line: self.data.lines, Text: self.data.text}[typ]
        ind = location.append(id=create_id())
        obj = typ(self, location[ind])
        self.graph_objects.append(obj)
        state['obj'] = obj
        return obj

    def undo_new_object(self, state):
        obj = state['obj']
        self.graph_objects.remove(obj)
        obj.id = '-' + obj.id
        self.redraw()

    def redo_new_object(self, state):
        obj = state['obj']
        self.graph_objects.append(obj)
        location = {Line: self.data.lines, Text: self.data.text}[type(obj)]
        obj.id = obj.id[1:]
        self.redraw()

    new_object = action_from_methods2('graph/new-object',
                                      new_object,
                                      undo_new_object,
                                      redo=redo_new_object)

    def delete_object(self, state, obj):
        obj.id = '-' + obj.id
        self.graph_objects.remove(obj)
        state['obj'] = obj
        self.redraw()

    delete_object = action_from_methods2('graph/delete-object',
                                         delete_object,
                                         redo_new_object,
                                         redo=undo_new_object)

    # add and remove datasets
    def add(self, state, x, y):
        ind = self.data.datasets.append(worksheet=x.worksheet.id,
                                        id=create_id(),
                                        x=x.name.encode('utf-8'),
                                        y=y.name.encode('utf-8'))

        d = Dataset(self, ind)
        self.datasets.append(d)
        pos = len(self.datasets) - 1
        #        print 'added dataset, index %d, position %d' % (ind, pos)

        d.connect('modified', self.on_dataset_modified)
        d.connect_signals()

        self.on_dataset_modified(d)
        self.emit('add-dataset', d)

        state['obj'] = d

        return pos

    def undo_add(self, state):
        d = state['obj']

        #        print 'undoing addition of dataset, index %d, position %d' % (d.ind, pos)
        self.datasets.remove(d)
        d.disconnect_signals()
        d.disconnect('modified', self.on_dataset_modified)
        self.emit('remove-dataset', d)
        self.redraw(True)
        d.id = '-' + d.id
#        self.data.datasets.delete(d.ind)

    def redo_add(self, state):
        d = state['obj']
        d.id = d.id[1:]
        self.datasets.append(d)
        d.connect('modified', self.on_dataset_modified)
        d.connect_signals()
        self.emit('add-dataset', d)
        self.redraw(True)

    add = action_from_methods2('graph_add_dataset',
                               add,
                               undo_add,
                               redo=redo_add)

    def remove(self, dataset):
        # we can do this even if `dataset` is a different object
        # than the one in self.datasets, if they have the same id
        # (see Dataset.__eq__)
        # TODO: why bother? just keep the object itself in the state
        ind = self.datasets.index(dataset)
        print 'removing dataset, index %d, position %d' % (dataset.ind, ind)
        dataset.id = '-' + dataset.id
        self.datasets.remove(dataset)
        try:
            dataset.disconnect('modified', self.on_dataset_modified)
        except NameError:
            pass
        self.emit('remove-dataset', dataset)
        self.redraw(True)
        return (dataset.ind, ind), None

    def undo_remove(self, data):
        ind, pos = data
        print 'undoing removal of dataset, index %d, position %d' % (ind, pos)
        dataset = Dataset(self, ind)
        dataset.id = dataset.id[1:]
        self.on_dataset_modified(dataset)
        self.datasets.insert(pos, dataset)
        dataset.connect('modified', self.on_dataset_modified)
        self.emit('add-dataset', dataset)
        self.redraw(True)

    remove = action_from_methods('graph_remove_dataset', remove, undo_remove)

    def on_dataset_modified(self, d=None):
        self.redraw(True)

    def paint_axes(self):
        for a in self.axes:
            a.paint_frame()

        self.grid_h.paint()
        self.grid_v.paint()

    def pos2y(self, pos):
        if pos.endswith('%'):
            return float(pos[:-1]) * self.plot_height / 100., '%'
        elif pos.endswith('y'):
            return self.data_to_phys(self.ymin, float(pos[:-1]))[1], 'y'
        elif pos.endswith('mm'):
            return float(pos[:-2]), 'mm'
        else:
            return float(pos), 'mm'

    def pos2x(self, pos):
        if pos.endswith('%'):
            return float(pos[:-1]) * self.plot_width / 100., '%'
        elif pos.endswith('x'):
            return self.data_to_phys(float(pos[:-1]), self.xmin)[0], 'x'
        elif pos.endswith('mm'):
            return float(pos[:-2]), 'mm'
        else:
            return float(pos), 'mm'

    def x2pos(self, x, typ):
        if typ == '%':
            return str(x * 100. / self.plot_width) + '%'
        elif typ == 'x':
            return str(self.phys_to_data(x, 0)[0]) + 'x'
        elif typ == 'mm':
            return str(x) + 'mm'

    def y2pos(self, y, typ):
        if typ == '%':
            return str(y * 100. / self.plot_height) + '%'
        elif typ == 'y':
            return str(self.phys_to_data(0, y)[1]) + 'y'
        elif typ == 'mm':
            return str(y) + 'mm'

    def data_to_phys(self, x, y):
        """
        Takes a point x,y in data coordinates and transforms to
        physical coordinates
        """
        x, xmin, xmax = map(self.axis_bottom.transform,
                            (x, self.xmin, self.xmax))
        y, ymin, ymax = map(self.axis_left.transform,
                            (y, self.ymin, self.ymax))

        px = self.plot_width * (x - xmin) / (xmax - xmin)
        py = self.plot_height * (y - ymin) / (ymax - ymin)

        #        bt = self.axis_bottom.transform
        #        lt = self.axis_left.transform
        #
        #        px = self.plot_width * (bt(x) - bt(self.xmin)) / (bt(self.xmax) - bt(self.xmin))
        #        py = self.plot_height * (bt(y) - bt(self.ymin)) / (bt(self.ymax) - bt(self.ymin))

        return px, py

    def phys_to_data(self, x, y):
        """
        Takes a point x,y in physical coordinates and transforms to
        data coordinates
        """
        xmin, xmax = map(self.axis_bottom.transform, (self.xmin, self.xmax))
        ymin, ymax = map(self.axis_left.transform, (self.ymin, self.ymax))

        px = x * (xmax - xmin) / self.plot_width + xmin
        py = y * (ymax - ymin) / self.plot_height + ymin

        return self.axis_bottom.invtransform(px), self.axis_left.invtransform(
            py)

    def mouse_to_phys(self, xm, ym):
        x = (xm / self.res) - self.marginl
        y = ((self.height_pixels - ym) / self.res) - self.marginb
        return x, y

    def mouse_to_data(self, xm, ym):
        x, y = self.mouse_to_phys(xm, ym)
        return self.phys_to_data(x, y)

    def autoscale(self):
        if len(self.datasets):
            xmin = min(d.xx.min() for d in self.datasets)
            xmax = max(d.xx.max() for d in self.datasets)
            ymin = min(d.yy.min() for d in self.datasets)
            ymax = max(d.yy.max() for d in self.datasets)
            self.zoom(xmin, xmax, ymin, ymax)

    def set_range(self, fr, to):
        self.fr, self.to = fr, to

    #####################
    # zoom action      #
    #####################

    def zoom_do(self, state, xmin, xmax, ymin, ymax):
        eps = 1e-24
        state['old'] = (self.xmin, self.xmax, self.ymin, self.ymax)
        if abs(xmin - xmax) <= eps or abs(ymin - ymax) <= eps:
            return
        self.xmin, self.xmax, self.ymin, self.ymax = xmin, xmax, ymin, ymax
        state['new'] = (xmin, xmax, ymin, ymax)

    def zoom_redo(self, state):
        self.xmin, self.xmax, self.ymin, self.ymax = state['new']
        self.reshape()
        self.redraw(True)

    def zoom_undo(self, state):
        self.xmin, self.xmax, self.ymin, self.ymax = state['old']
        self.reshape()
        self.redraw(True)

    def zoom_combine(self, state, other):
        return False

    zoom = action_from_methods2('graph-zoom',
                                zoom_do,
                                zoom_undo,
                                redo=zoom_redo,
                                combine=zoom_combine)

    def zoomout(self, x1, x2, x3, x4):
        if x3 == x4:
            return x1, x2
        a = (x2 - x1) / (x4 - x3)
        c = x1 - a * x3
        f1 = a * x1 + c
        f2 = a * x2 + c
        return min(f1, f2), max(f1, f2)

    def init(self):
        glClearColor(*self.background_color)
        glClear(GL_COLOR_BUFFER_BIT)

        # enable transparency
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

        glDisable(GL_DEPTH_TEST)
        glShadeModel(GL_FLAT)

        # we need this to render pil fonts properly
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
        glPixelStorei(GL_PACK_ALIGNMENT, 1)

        self.listno = glGenLists(1)

    def display(self, width=-1, height=-1):
        if not hasattr(self, 'listno'):
            return
        if width == -1 and height == -1:
            width, height = self.last_width, self.last_height
        else:
            self.last_width, self.last_height = width, height

        if not self.paint_xor_objects:
            if self.recalc:
                if not self.ps:
                    for i, d in enumerate(self.datasets):
                        glClearColor(*self.background_color)
                        glClear(GL_COLOR_BUFFER_BIT)

                        w, h, _, renderer = self.textpainter.render_text_chunk_symbol(
                            str(i))
                        if renderer is not None:
                            renderer((w / 2) / self.res, (h / 2) / self.res)

                            i = wx.EmptyImage(w, h)
                            data = glReadPixels(int(self.marginl * self.res),
                                                int(self.marginb * self.res),
                                                int(w), int(h), GL_RGB,
                                                GL_UNSIGNED_BYTE)
                            i.SetData(data)
                            d._legend_wxbitmap = i.ConvertToBitmap()
                    self.emit('shape-changed')

                glDeleteLists(self.listno, 1)
                glNewList(self.listno, GL_COMPILE)

                glClearColor(*self.background_color)
                glClear(GL_COLOR_BUFFER_BIT)

                # set up clipping
                glClipPlane(GL_CLIP_PLANE0, [1, 0, 0, 0])
                glClipPlane(GL_CLIP_PLANE1, [-1, 0, 0, self.plot_width])
                glClipPlane(GL_CLIP_PLANE2, [0, 1, 0, 0])
                glClipPlane(GL_CLIP_PLANE3, [0, -1, 0, self.plot_height])

                if len(self.datasets):
                    for plane in [
                            GL_CLIP_PLANE0, GL_CLIP_PLANE1, GL_CLIP_PLANE2,
                            GL_CLIP_PLANE3
                    ]:
                        glEnable(plane)

                for d in self.datasets:
                    d.paint()
                for f in self.functions:
                    f.paint()

                if len(self.datasets):
                    for plane in [
                            GL_CLIP_PLANE0, GL_CLIP_PLANE1, GL_CLIP_PLANE2,
                            GL_CLIP_PLANE3
                    ]:
                        glDisable(plane)

                self.paint_axes()

                glEndList()
                self.recalc = False

            glCallList(self.listno)

            for axis in self.axes:
                axis.paint_text()
                axis.paint_title()

            for o in self.graph_objects:
                o.draw()
                if self.mode == 'arrow' and self.selected_object == o:
                    o.draw_handles()
        else:
            glLogicOp(GL_XOR)
            glEnable(GL_COLOR_LOGIC_OP)
            for o in self.objects:
                o.redraw()
            glDisable(GL_COLOR_LOGIC_OP)

    def reshape(self, width=-1, height=-1):
        if not hasattr(self, 'listno'):
            return
        t = time.time()
        if width == -1 and height == -1:
            width, height = self.last_width, self.last_height
        else:
            self.last_width, self.last_height = width, height

        # aspect ratio (width/height)
        self.aspect = self.pwidth / self.pheight

        # resolution (in pixels/mm)
        self.res = min(width / self.pwidth, height / self.pheight)
        displaydpi = 100.
        self.displayres = displaydpi / 25.4  # 25.4 = mm/inch
        self.magnification = self.res / self.displayres

        # set width and height
        self.width_pixels, self.height_pixels = width, height
        self.width_mm = width / self.res
        self.height_mm = height / self.res

        # measure titles
        facesize = self.axis_title_font_size * self.magnification
        if self.xtitle != '':
            _, tith = self.textpainter.render_text(self.xtitle,
                                                   facesize,
                                                   0,
                                                   0,
                                                   measure_only=True)
        else:
            tith = 0

        if self.ytitle != '':
            titw, _ = self.textpainter.render_text(self.ytitle,
                                                   facesize,
                                                   0,
                                                   0,
                                                   measure_only=True,
                                                   orientation='v')
        else:
            titw = 0

        # measure tick labels
        try:
            self.ticw = max(
                self.textpainter.render_text(self.axis_left.totex(y),
                                             facesize,
                                             0,
                                             0,
                                             measure_only=True)[0]
                for y in self.axis_left.tics(self.ymin, self.ymax)[0])  # :-)
        except ValueError:
            self.ticw = 0

        try:
            self.tich = max(
                self.textpainter.render_text(self.axis_bottom.totex(x),
                                             facesize,
                                             0,
                                             0,
                                             measure_only=True)[1]
                for x in self.axis_bottom.tics(self.xmin, self.xmax)[0])
        except ValueError:
            self.tich = 0

        # set margins (units are in mm)
        self.marginb = tith + self.tich + self.axis_title_font_size * self.magnification / 2 + 2
        self.margint = self.height_mm * 0.03
        self.marginl = titw + self.ticw + self.axis_title_font_size * self.magnification / 2 + 2
        self.marginr = self.width_mm * 0.03

        if self.width_mm / self.height_mm > self.aspect:
            self.marginr += (self.width_mm - self.height_mm * self.aspect) / 2
            self.marginl += (self.width_mm - self.height_mm * self.aspect) / 2
        else:
            self.margint += (self.height_mm - self.width_mm / self.aspect) / 2
            self.marginb += (self.height_mm - self.width_mm / self.aspect) / 2

        self.plot_width = self.width_mm - self.marginl - self.marginr
        self.plot_height = self.height_mm - self.margint - self.marginb

        # resize the viewport
        glViewport(0, 0, int(width), int(height))
        self.viewport = glGetIntegerv(GL_VIEWPORT)

        # set opengl projection matrix with the origin
        # at the bottom left corner # of the graph
        # and scale in mm
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glTranslated(-1. + 2. * self.marginl / self.width_mm,
                     -1. + 2. * self.marginb / self.height_mm, 0)
        glScaled(2. / self.width_mm, 2. / self.height_mm, 1)
#        print >>sys.stderr, 'R: ', time.time()-t, "seconds"

    def export_ascii(self, outfile):
        # mathtext is not rendered directly
        self.pstext = []

        save = self.width_pixels, self.height_pixels
        self.reshape(self.pwidth * self.displayres,
                     self.pheight * self.displayres)

        d = tempfile.mkdtemp()
        filename = self.name + '.eps'
        f = open(os.path.join(d, filename), 'wb')

        gl2ps_BeginPage("Title", "Producer", self.viewport, f, filename)
        self.ps = True
        self.recalc = True
        self.display()
        self.ps = False
        gl2ps_EndPage()

        f.close()

        self.reshape(*save)

        f = open(d + '/' + filename, 'rb')
        for line in f:
            if line == '%%EndProlog\n':
                # insert encoded mathtext fonts
                # at the end of the prolog
                type42 = []
                type42.append(FONTFILE)
                for fn in ['r', 'ex', 'mi', 'sy', 'tt']:
                    type42.append(
                        os.path.join(DATADIR, 'data', 'fonts', 'bakoma-cm',
                                     'cm%s10.ttf' % fn))
                for font in type42:
                    print >> outfile, "%%BeginFont: " + FT2Font(
                        str(font)).postscript_name
                    print >> outfile, encodeTTFasPS(font)
                    print >> outfile, "%%EndFont"
                outfile.write(line)
            elif line == 'showpage\n':
                # insert mathtext chunks
                # at the end of the file
                outfile.write(''.join(self.pstext))
                outfile.write(line)
            else:
                # copy lines
                outfile.write(line)
        f.close()

    def show(self):
        for d in self.datasets:
            if not hasattr(d, 'xx'):
                d.recalculate()

    def button_press(self, x, y, button=None):
        if self.mode == 'zoom':
            if button in (1, 3):
                self.paint_xor_objects = True
                self.pixx, self.pixy = x, y
                self.ix, self.iy = self.mouse_to_phys(x, y)
                self.rubberband.show(self.ix, self.iy, self.ix, self.iy)
                self.redraw()
            if button == 2:
                self.haha = True
            else:
                self.haha = False
        elif self.mode == 'hand':
            if self.selected_function is not None:
                self.selected_function.set_reg(False)
                self.selected_function.move(*self.mouse_to_data(x, y))
                #                self.emit('redraw')
                self._movefunc = DrawFunction(self, self.selected_function)
                self.objects.append(self._movefunc)
                self.paint_xor_objects = True
                self._movefunc.show(*self.mouse_to_data(x, y))
                self.redraw()
        elif self.mode == 's-reader':
            self.paint_xor_objects = True
            self.cross.show(*self.mouse_to_phys(x, y))
            self.redraw()
            self.emit('status-message', '%f, %f' % self.mouse_to_data(x, y))
        elif self.mode == 'range':
            self.paint_xor_objects = True
            self.rangehandle.show(*self.mouse_to_phys(x, y))
            self.redraw()
        elif self.mode == 'd-reader':
            qx, qy = self.mouse_to_data(x, y)

            distances = []
            closest_ind = []

            for d in self.datasets:
                dist = (d.xx - qx) * (d.xx - qx) + (d.yy - qy) * (d.yy - qy)
                arg = argmin(dist)
                closest_ind.append(arg)
                distances.append(dist[arg])

            ind = argmin(distances)
            dataset = self.datasets[ind]
            x, y = dataset.xx[closest_ind[ind]], dataset.yy[closest_ind[ind]]

            self.paint_xor_objects = True
            self.cross.show(*self.data_to_phys(x, y))
            self.redraw()
            self.emit('status-message', '%f, %f' % (x, y))
        elif self.mode == 'arrow':
            if button == 1:
                x, y = self.mouse_to_phys(x, y)
                for o in self.graph_objects:
                    if o.hittest(x, y):
                        self.selected_object = o
                        self.dragobj = o
                        self.dragobj.rec = False
                        self.dragobj_xor = Move(self.dragobj)
                        self.objects.append(self.dragobj_xor)
                        self.paint_xor_objects = True
                        self.dragobj_xor.show(x, y)
                        if o.hittest_handles(x, y):
                            self.dragobj.dragstart = None
                        break
                else:
                    self.selected_object = None
                self.redraw()
            elif button == 3:
                self.emit('right-clicked', None)
                print >> sys.stderr, 'right-clicked', None
        elif self.mode in ('draw-line', 'draw-text'):
            xi, yi = self.mouse_to_phys(x, y)
            createobj = self.new_object({
                'draw-line': Line,
                'draw-text': Text
            }[self.mode])
            createobj.begin(xi, yi)

            self.dragobj = createobj
            self.dragobj_xor = Move(self.dragobj)
            self.objects.append(self.dragobj_xor)

            self.paint_xor_objects = True
            self.dragobj_xor.show(xi, yi)
            self.selected_object = createobj
            self.mode = 'arrow'
            self.redraw()
            self.emit('request-cursor', 'arrow')

    def button_doubleclick(self, x, y, button):
        if self.mode == 'arrow' and button == 1:
            x, y = self.mouse_to_phys(x, y)
            for o in self.graph_objects:
                if o.hittest_handles(x, y):
                    self.emit('object-doubleclicked', o)
                    o.emit('modified')
                    break

    def button_release(self, x, y, button):
        if self.mode == 'zoom':
            if button == 2:
                self.autoscale()
                self.redraw(True)
            elif button == 1 or button == 3:
                self.rubberband.hide()
                self.redraw()
                self.paint_xor_objects = False

                zix, ziy = self.mouse_to_data(self.pixx, self.pixy)
                zfx, zfy = self.mouse_to_data(x, y)

                _xmin, _xmax = min(zix, zfx), max(zix, zfx)
                _ymin, _ymax = min(zfy, ziy), max(zfy, ziy)

                if button == 3:
                    _xmin, _xmax = self.axis_bottom.transform(
                        _xmin), self.axis_bottom.transform(_xmax)
                    _ymin, _ymax = self.axis_left.transform(
                        _ymin), self.axis_left.transform(_ymax)

                    xmin, xmax = self.zoomout(
                        self.axis_bottom.transform(self.xmin),
                        self.axis_bottom.transform(self.xmax), _xmin, _xmax)
                    ymin, ymax = self.zoomout(
                        self.axis_left.transform(self.ymin),
                        self.axis_left.transform(self.ymax), _ymin, _ymax)

                    xmin, xmax = self.axis_bottom.invtransform(
                        xmin), self.axis_bottom.invtransform(xmax)
                    ymin, ymax = self.axis_left.invtransform(
                        ymin), self.axis_left.invtransform(ymax)
                else:
                    xmin, xmax, ymin, ymax = _xmin, _xmax, _ymin, _ymax
                self.zoom(xmin, xmax, ymin, ymax)
                self.reshape()
                self.redraw(True)
        elif self.mode == 'hand':
            if self.selected_function is not None:
                self.selected_function.set_reg(True)
                self.selected_function.move(*self.mouse_to_data(x, y))
                del self.objects[-1]
                self.paint_xor_objects = False
                self.redraw(True)
        elif self.mode == 's-reader':
            self.cross.hide()
            self.redraw()
            self.paint_xor_objects = False
        elif self.mode == 'd-reader':
            self.cross.hide()
            self.redraw()
            self.paint_xor_objects = False

        elif self.mode == 'arrow':
            if button == 1:
                if self.dragobj is not None:
                    self.dragobj.rec = True
                    self.dragobj.record_position()
                    self.dragobj = None
                    self.dragobj_xor.hide()
                    self.objects.remove(self.dragobj_xor)
                    self.paint_xor_objects = False
                    self.redraw()
        elif self.mode == 'range':
            if button is None:
                button = self.__button
            else:
                self.__button = button

            x, y = self.mouse_to_data(x, y)
            for d in self.selected_datasets:
                if button == 1:
                    d.range = (x, d.range[1])
                elif button == 3:
                    d.range = (d.range[0], x)
                elif button == 2:
                    d.range = (-inf, inf)
            self.rangehandle.hide()
            self.redraw()
            self.paint_xor_objects = False

    def button_motion(self, x, y, dragging):
        if self.mode == 'zoom' and dragging and hasattr(self, 'ix'):
            self.rubberband.move(self.ix, self.iy, *self.mouse_to_phys(x, y))
            self.redraw()
        elif self.mode == 'range' and dragging:
            self.rangehandle.move(*self.mouse_to_phys(x, y))
            self.redraw()


#            self.button_press(x, y)
        elif self.mode == 'hand' and dragging:
            if self.selected_function is not None:
                self.selected_function.move(*self.mouse_to_data(x, y))
                self._movefunc.move(*self.mouse_to_data(x, y))
                self.redraw()
        elif self.mode == 's-reader' and dragging:
            self.cross.move(*self.mouse_to_phys(x, y))
            self.redraw()
            self.emit('status-message', '%f, %f' % self.mouse_to_data(x, y))
        elif self.mode == 'd-reader' and dragging:
            qx, qy = self.mouse_to_data(x, y)
            distances = []
            closest_ind = []

            for d in self.datasets:
                dist = (d.xx - qx) * (d.xx - qx) + (d.yy - qy) * (d.yy - qy)
                arg = argmin(dist)
                closest_ind.append(arg)
                distances.append(dist[arg])

            ind = argmin(distances)
            dataset = self.datasets[ind]
            x, y = dataset.xx[closest_ind[ind]], dataset.yy[closest_ind[ind]]

            self.cross.move(*self.data_to_phys(x, y))
            self.redraw()
            self.emit('status-message', '%f, %f' % (x, y))
        elif self.mode == 'arrow':
            if not hasattr(self, 'res'):
                # not initialized yet, do nothing
                return
            x, y = self.mouse_to_phys(x, y)
            if self.dragobj is not None:  # drag a handle on an object
                self.dragobj_xor.move(x, y)
                self.redraw()
                self.emit('request-cursor', 'none')
            else:  # look for handles
                for o in self.graph_objects:
                    if o.hittest(x, y):
                        self.emit('request-cursor', 'hand')
                        break
                else:
                    self.emit('request-cursor', 'arrow')

    def key_down(self, keycode):
        import wx
        if keycode == wx.WXK_DELETE and self.selected_object is not None:
            self.delete_object(self.selected_object)

    _xtype = wrap_attribute('xtype')
    _ytype = wrap_attribute('ytype')
    _xtitle = wrap_attribute('xtitle')
    _ytitle = wrap_attribute('ytitle')
    _zoom = wrap_attribute('zoom')