import drawgraph class BoundingBox: #TODO this should be view ... def __init__(self, node, xcenter, ycenter, view): self.xcenter, self.ycenter = xcenter,ycenter self.boxwidth = int(node.w * view.scale) #TODO self.boxheight = int(node.h * view.scale) self.wmax = self.boxwidth self.hmax = self.boxheight def finalize (self): self.xleft = self.xcenter - self.wmax//2 self.xright = self.xcenter + self.wmax//2 self.ytop = self.ycenter - self.hmax//2 self.x = self.xcenter-self.boxwidth//2 self.y = self.ycenter-self.boxheight//2 class Graph: def __init__(self): self.fixedfont = True self.nodes = [] self.edges = [] def __equals__ (self, other): #fixedfont TODO return other.nodes == self.nodes and other.edges == self.edges class MyGraphRenderer (drawgraph.GraphRenderer): def __init__ (self, driver, scale=75): self.driver = driver self.textzones = [] self.highlightwords = [] def setscale(self, scale, graph): scale = max(min(scale, self.SCALEMAX), self.SCALEMIN) self.scale = float(scale) w, h = graph.boundingbox self.margin = int(self.MARGIN * scale) self.width = int(w * scale) + (2 * self.margin) self.height = int(h * scale) + (2 * self.margin) self.bboxh = h size = int(15 * (scale-10) / 75) self.font = self.driver.getfont (size, graph) def computevisible (self, graph, view): visible = Graph() for node in graph.nodes.values(): if view.visible_node (node.x,node.y, node.w, node.h): visible.nodes.append(node) for edge in graph.edges: x1, y1, x2, y2 = edge.limits() if view.visible_edge (x1, y1, x2, y2): visible.edges.append (edge) return visible def draw_background (self, view): bbox = view.getboundingbox() ox, oy, width, height = bbox dpy_width, dpy_height = self.driver.size # some versions of the SDL misinterpret widely out-of-range values, # so clamp them if ox < 0: width += ox ox = 0 if oy < 0: height += oy oy = 0 if width > dpy_width: width = dpy_width if height > dpy_height: height = dpy_height self.driver.fill((224, 255, 224), (ox, oy, width, height)) # gray off-bkgnd areas gray = (128, 128, 128) if ox > 0: self.driver.fill(gray, (0, 0, ox, dpy_height)) if oy > 0: self.driver.fill(gray, (0, 0, dpy_width, oy)) w = dpy_width - (ox + width) if w > 0: self.driver.fill(gray, (dpy_width-w, 0, w, dpy_height)) h = dpy_height - (oy + height) if h > 0: self.driver.fill(gray, (0, dpy_height-h, dpy_width, h)) def render (self, graph, view): # self.driver.setscale (scale,graph) the view has the scale TODO # self.driver.setoffset (0,0) the view knows the offset visible = self.computevisible (graph, view) self.draw_background (view) # draw the graph and record the position of texts del self.textzones[:] for cmd in self.draw_commands (visible, view): cmd() def draw_image (self, img, bbox): w, h = img.get_size() img.draw (bbox.xcenter-w//2, bbox.ytop+bbox.y) #XXXX WTF ??? def if_font_none (self, commands, lines, graph, bgcolor, bbox): if lines: raw_line = lines[0].replace('\\l','').replace('\r','') if raw_line: for size in (12, 10, 8, 6, 4): font = self.driver.getfont (size, graph.fixedfont) img = MyTextSnippet(self.driver, self, font, raw_line, (0, 0, 0), bgcolor) w, h = img.get_size() if (w >= bbox.boxwidth or h >= bbox.boxheight): continue else: if w > bbox.wmax: bbox.wmax = w def cmd(bbox, img=img): self.draw_image (img, bbox) commands.append (cmd) bbox.hmax += h break def draw_node_commands (self, node, graph, bbox, view): xcenter, ycenter = view.map(node.x, node.y) boxwidth = int(node.w * view.scale) #TODO boxheight = int(node.h * view.scale) fgcolor = self.driver.getcolor(node.color, (0,0,0)) bgcolor = self.driver.getcolor(node.fillcolor, (255,255,255)) if node.highlight: fgcolor = self.driver.highlight_color(fgcolor) bgcolor = self.driver.highlight_color(bgcolor) text = node.label lines = text.replace('\\l','\\l\n').replace('\r','\r\n').split('\n') # ignore a final newline if not lines[-1]: del lines[-1] commands = [] bkgndcommands = [] self.font = None if self.font is None: self.if_font_none(commands, lines, graph, bgcolor, bbox) else: for line in lines: raw_line = line.replace('\\l','').replace('\r','') or ' ' img = MyTextSnippet(self, raw_line, (0, 0, 0), bgcolor) w, h = img.get_size() if w > bbox.wmax: bbox.wmax = w if raw_line.strip(): if line.endswith('\\l'): def cmd(bbox,img=img): img.draw(view.xleft, view.ytop+view.y) elif line.endswith('\r'): def cmd(bbox, img=img): img.draw(view.xright-view.w, view.ytop+view.y) else: def cmd(bbox, img=img): img.draw(view.xcenter-view.w//2, view.ytop+y) commands.append(cmd) bbox.hmax += h #hmax += 8 # we know the bounding box only now; setting these variables will # have an effect on the values seen inside the cmd() functions above self.draw_node_shape (node, bkgndcommands, commands, bbox, boxheight, boxwidth, bgcolor, fgcolor) return bkgndcommands, commands def draw_node_shape (self, node, bkgndcommands, commands, view, boxheight, boxwidth, bgcolor, fgcolor): if node.shape == 'box': def cmd (view): rect = (view.x-1, view.y-1, boxwidth+2, boxheight+2) self.driver.fill(bgcolor, rect) bkgndcommands.append (cmd) def cmd(view): #pygame.draw.rect(self.screen, fgcolor, rect, 1) rect = (view.x-1, view.y-1, boxwidth+2, boxheight+2) self.driver.draw_rect (fgcolor, rect, 1) commands.append(cmd) elif node.shape == 'ellipse': def cmd(view): rect = (view.x-1, view.y-1, boxwidth+2, boxheight+2) self.driver.draw_ellipse(bgcolor, rect, 0) bkgndcommands.append(cmd) def cmd(view): rect = (view.x-1, view.y-1, boxwidth+2, boxheight+2) self.driver.draw_ellipse(fgcolor, rect, 1) commands.append(cmd) elif node.shape == 'octagon': import math def cmd(view): step = 1-math.sqrt(2)/2 points = [(int(view.x+boxwidth*fx), int(view.y+boxheight*fy)) for fx, fy in [(step,0), (1-step,0), (1,step), (1,1-step), (1-step,1), (step,1), (0,1-step), (0,step)]] self.driver.draw_polygon(bgcolor, points, 0) bkgndcommands.append(cmd) def cmd(view): step = 1-math.sqrt(2)/2 points = [(int(view.x+boxwidth*fx), int(view.y+boxheight*fy)) for fx, fy in [(step,0), (1-step,0), (1,step), (1,1-step), (1-step,1), (step,1), (0,1-step), (0,step)]] self.driver.draw_polygon(fgcolor, points, 1) commands.append(cmd) def draw_edge_body (self, points, fgcolor): #pygame.draw.lines(self.screen, fgcolor, False, points) self.driver.draw_lines (fgcolor, False, points) def draw_edge (self, edge, edgebodycmd, edgeheadcmd, view): fgcolor = self.driver.getcolor(edge.color, (0,0,0)) if edge.highlight: fgcolor = self.driver.highlight_color(fgcolor) points = [view.map(*xy) for xy in edge.bezierpoints()] edgebodycmd.append (Command (self.draw_edge_body, points, fgcolor)) points = [view.map(*xy) for xy in edge.arrowhead()] if points: def drawedgehead(points=points, fgcolor=fgcolor): self.driver.draw_polygon(fgcolor, points, 0) edgeheadcmd.append(drawedgehead) if edge.label: x, y = view.map(edge.xl, edge.yl) font = self.driver.getfont (10) #TODO style img = MyTextSnippet (self.driver, self, font, edge.label, (0, 0, 0)) w, h = img.get_size() if view.visible(x-w//2, y-h//2, x+w//2, y+h//2): def drawedgelabel(img=img, x1=x-w//2, y1=y-h//2): img.draw(x1, y1) edgeheadcmd.append(drawedgelabel) def draw_commands(self, graph, view): nodebkgndcmd = [] nodecmd = [] for node in graph.nodes: xcenter,ycenter = view.map (node.x, node.y) bbox = BoundingBox (node, xcenter,ycenter, view) cmd1, cmd2 = self.draw_node_commands(node, graph, bbox,view) bbox.finalize() nodebkgndcmd += [ Command (c, bbox) for c in cmd1 ] nodecmd += [ Command (c, bbox) for c in cmd2 ] edgebodycmd = [] edgeheadcmd = [] for edge in graph.edges: self.draw_edge(edge, edgebodycmd, edgeheadcmd, view) return edgebodycmd + nodebkgndcmd + edgeheadcmd + nodecmd class Command: def __init__ (self, fun, *args): self.fun = fun self.args = args def __call__ (self): self.fun (*self.args) from drawgraph import TextSnippet class MyTextSnippet (TextSnippet): def __init__(self, driver, renderer, font, text, fgcolor, bgcolor=None): self.renderer = renderer self.driver = driver self.imgs = [] self.parts = [] import re re_nonword=re.compile(r'([^0-9a-zA-Z_.]+)') parts = self.parts for word in re_nonword.split(text): if not word: continue if word in renderer.highlightwords: fg, bg = renderer.wordcolor(word) bg = bg or bgcolor else: fg, bg = fgcolor, bgcolor parts.append((word, fg, bg)) # consolidate sequences of words with the same color for i in range(len(parts)-2, -1, -1): if parts[i][1:] == parts[i+1][1:]: word, fg, bg = parts[i] parts[i] = word + parts[i+1][0], fg, bg del parts[i+1] # delete None backgrounds for i in range(len(parts)): if parts[i][2] is None: parts[i] = parts[i][:2] # render parts i = 0 while i < len(parts): part = parts[i] word = part[0] img = self.driver.render_font (font, word, False, *part[1:]) if img: self.imgs.append(img) i += 1 try: try: img = font.render(word, False, *part[1:]) except pygame.error, e: # Try *with* anti-aliasing to work around a bug in SDL img = font.render(word, True, *part[1:]) except pygame.error: del parts[i] # Text has zero width else: self.imgs.append(img) i += 1 def draw (self, x, y): for part, img in zip(self.parts, self.imgs): word = part[0] self.renderer.driver.blit_image (img, (x, y)) w, h = img.get_size() self.renderer.textzones.append((x, y, w, h, word)) x += w