def __init__(self): gobject.GObject.__init__(self) self.__contexts = dict() self.__annotations = dict() self.__counter = 0 self.__colors = Colors()
class Annotations(gobject.GObject): """ Provides the data model for annotations. It keeps track of all annotations and informs listeners if something changed. +---------------------+-------------------+-------------------------+ |Signal | Signature | Description | +=====================+===================+=========================+ |"annotation-added" | :class:`int`, | Called with id, color, | | | :class:`str`, | start-time and end-time | | | :class:`float`, | when a new annotation | | | :class:`float` | is added. | +---------------------+-------------------+-------------------------+ |"annotation-removed" | :class:`int` | Called with the id of | | | | an annotation that has | | | | been removed. | +---------------------+-------------------+-------------------------+ |"annotation-changed" | :class:`int`, | Called with the id, | | | :class:`str`, | context-name, color, | | | :class:`str`, | start- and end-time of | | | :class:`float`, | an annotation that has | | | :class:`float` | been changed. | +---------------------+-------------------+-------------------------+ |"context-added" | :class:`str`, | Called with the name and| | | :class:`str` | color of a context that | | | | has been added. | +---------------------+-------------------+-------------------------+ | "context-removed" | :class:`str` | Called with the name of | | | | a context that has been | | | | removed. | +---------------------+-------------------+-------------------------+ """ __metaclass__ = AnnotationsMeta def __init__(self): gobject.GObject.__init__(self) self.__contexts = dict() self.__annotations = dict() self.__counter = 0 self.__colors = Colors() def add_annotation(self,ctx,boundl,boundr): """ :param ctx: The context name :type ctx: :class:`str` :param boundl: The start time :type boundl: :class:`float` :param boundr: The end time :type boundr: :class:`float` :returns: The id of the new annotation :rtype: :class:`int` Adds a new annotation for the context *ctx* and the start time *boundl* and end time *boundr*. """ if ctx not in self.__contexts: self.add_context(ctx) id = self.__counter self.__counter += 1 (color,entries) = self.__contexts[ctx] self.__annotations[id] = (ctx,boundl,boundr) entries.add(id) self.emit('annotation-added',id,color,boundl,boundr) return id def remove_annotation(self,id): """ :param id: The id of the annotation to be removed :type id: :class:`int` Removes an annotation from the model. """ (ctx,boundl,boundr) = self.__annotations[id] (color,entries) = self.__contexts[ctx] entries.remove(id) del self.__annotations[id] self.emit('annotation-removed',id) def add_context(self,ctx): """ :param ctx: Name of the new context :type ctx: :class:`str` :returns: Generated color for the context :rtype: :class:`str` Adds a new context type to the model and generates a color for it. If the context already exists the color is looked up.""" if ctx not in self.__contexts: color = self.free_color() if color is None: raise "HALP! I CAN'T HAZ COLOR" self.__contexts[ctx] = (color,set()) self.emit('context-added',ctx,color) return color else: (color,entries) = self.__contexts[ctx] return color def free_color(self): return rgb2hex(self.__colors.next()) def remove_context(self,ctx): """ :param ctx: The name of the context :type ctx: :class:`str` Removes a context from the model. The color used by the context becomes available to new contexts. """ if ctx not in self.__contexts: return (color,entries) = self.__contexts[ctx] for id in entries: del self.__annotations[id] self.emit('annotation-removed',id) self.emit('context-removed',ctx) del self.__contexts[ctx] def find_annotation(self,x): """ :param x: The time where to search :type x: :class:`float` :returns: A list of all annotations that include the timestamp :rtype: \[ :class:`int` \] Finds all annotations that include a given timestamp. If the timestamp lies within no annotation, it returns :const:`[]`.""" hits = [] for (id,(ctx,boundl,boundr)) in self.__annotations.iteritems(): if x>=boundl and x<=boundr: hits.append(id) return hits def get_annotation(self,id): """ :param id: The id of the annotation :type id: :class:`int` :returns: Context name, color, start-time and end-time of the annotation :rtype: (:class:`str`, :class:`str`, :class:`float`, :class:`float`)""" (ctx,boundl,boundr) = self.__annotations[id] (color,entries) = self.__contexts[ctx] return (ctx,color,boundl,boundr) def contexts(self): """ :returns: An iterator over name and colors of all contexts in the model :rtype: :class:`iterator`""" return((name,color) for name,(color,entries) in self.__contexts.iteritems()) def annotations(self): """ :returns: An iterator over id, color, start-time and end-time of all annotations in the model. :rtype: :class:`iterator`""" return((id,self.__contexts[ctx][0],boundl,boundr) for (id,(ctx,boundl,boundr)) in self.__annotations.iteritems()) def __iter__(self): return self.__annotations.itervalues() def bounds(self): """ :rtype: (:class:`float`, :class:`float`) Calculate the minimal start-time and maximal end-time of all annotations.""" l = None r = None for (ctx,boundl,boundr) in self.__annotations.itervalues(): if l is None or boundl < l: l = boundl if r is None or boundr > r: r = boundr return (l,r) def clear(self): """ Removes all annotations and contexts from the model. """ for i in self.__annotations: self.emit('annotation-removed',i) for c in self.__contexts: self.emit('context-removed',c) self.__annotations = dict() self.__contexts = dict() self.__counter = 0 def find_boundings(self,x,exclude=None): """ :param x: The x-position :type x: :class:`float` :param exclude: A context id not to consider or :const:`None` to consider all :type exclude: :class:`int` or :class:`None` :returns: The two nearest annotations as a tuple. Each can be :const:`None` if there is no annotation to the left or right. :rtype: (:class:`int`, :class:`int`) Given a x-position, it finds the two annotations nearest to the position on the left and right. """ curl = None curr = None for (id,(ctx,boundl,boundr)) in self.__annotations.iteritems(): if id is exclude: continue if boundl > x and (curr is None or curr[1] > boundl): curr = (id,boundl) if boundr < x and (curl is None or curl[1] < boundr): curl = (id,boundr) return (curl,curr) def update_annotation(self,id,boundl,boundr): """ :param id: The id of the annotation :type id: :class:`int` :param boundl: The new start-time for the annotation :type boundl: :class:`float` :param boundr: The new end-time for the annotation :type boundr: :class:`float` Changes an annotation to new time-bounds. """ (ctx,oldl,oldr) = self.__annotations[id] self.__annotations[id] = (ctx,boundl,boundr) (color,entries) = self.__contexts[ctx] self.emit('annotation-changed',id,ctx,color,boundl,boundr) def write(self,fn): """ :param fn: The filename to write to :type fn: :class:`str` Writes the annotations to a file. The format is <context-name> <start-time> <end-time> The timestamps are seconds since 1.1.1970 (UNIX timestamps)""" utc = UTC() with open(fn,'w') as h: for (ctx,boundl,boundr) in self.__annotations.itervalues(): h.write(ctx+" "+str(calendar.timegm(num2date(boundl,utc).utctimetuple()))+" " +str(calendar.timegm(num2date(boundr,utc).utctimetuple()))+"\n") def read(self,fn): """ :param fn: The filename from which to read :type fn: string The inverse of :func:`write`.""" self.clear() utc = UTC() with open(fn,'r') as h: c = 0 for ln in h: c += 1 words = ln.split() if len(words) is not 3: raise IOError("Line "+str(c)+" of "+fn+" has "+str(len(words))+" columns (must have 3)") (name,start,end) = ln.split() try: tstart = date2num(datetime.datetime.utcfromtimestamp(float(start))) tend = date2num(datetime.datetime.utcfromtimestamp(float(end))) except ValueError: raise IOError("Line "+str(c)+" of "+fn+" contains invalid timestamps") self.add_annotation(name,tstart,tend)