"""A custom graphic renderer for the '.plain' files produced by dot."""from__future__importgeneratorsimportre,os,mathimportpygamefrompygame.localsimport*this_dir=os.path.dirname(os.path.abspath(__file__))FONT=os.path.join(this_dir,'cyrvetic.ttf')FIXEDFONT=os.path.join(this_dir,'VeraMoBd.ttf')COLOR={'black':(0,0,0),'white':(255,255,255),'red':(255,0,0),'green':(0,255,0),'blue':(0,0,255),'yellow':(255,255,0),}re_nonword=re.compile(r'([^0-9a-zA-Z_.]+)')defcombine(color1,color2,alpha):r1,g1,b1=color1r2,g2,b2=color2beta=1.0-alphareturn(int(r1*alpha+r2*beta),int(g1*alpha+g2*beta),int(b1*alpha+b2*beta))defhighlight_color(color):ifcolor==(0,0,0):# black becomes magentareturn(255,0,255)elifcolor==(255,255,255):# white becomes yellowreturn(255,255,0)intensity=sum(color)ifintensity>191*3:returncombine(color,(128,192,0),0.2)else:returncombine(color,(255,255,0),0.2)defgetcolor(name,default):ifnameinCOLOR:returnCOLOR[name]elifname.startswith('#')andlen(name)==7:rval=COLOR[name]=(int(name[1:3],16),int(name[3:5],16),int(name[5:7],16))returnrvalelse:returndefaultclassGraphLayout:fixedfont=Falsedef__init__(self,scale,width,height):self.scale=scaleself.boundingbox=width,heightself.nodes={}self.edges=[]self.links={}defadd_node(self,*args):n=Node(*args)self.nodes[n.name]=ndefadd_edge(self,*args):self.edges.append(Edge(self.nodes,*args))defget_display(self):fromgraphdisplayimportGraphDisplayreturnGraphDisplay(self)defdisplay(self):self.get_display().run()defreload(self):returnself# async interaction helpersdefdisplay_async_quit():pygame.event.post(pygame.event.Event(QUIT))defdisplay_async_cmd(**kwds):pygame.event.post(pygame.event.Event(USEREVENT,**kwds))EventQueue=[]defwait_for_events():ifnotEventQueue:EventQueue.append(pygame.event.wait())EventQueue.extend(pygame.event.get())defwait_for_async_cmd():# wait until another thread pushes a USEREVENT in the queuewhileTrue:wait_for_events()e=EventQueue.pop(0)ife.typein(USEREVENT,QUIT):# discard all other eventsbreakEventQueue.insert(0,e)# re-insert the event for further processingclassNode:def__init__(self,name,x,y,w,h,label,style,shape,color,fillcolor):self.name=nameself.x=float(x)self.y=float(y)self.w=float(w)self.h=float(h)self.label=labelself.style=styleself.shape=shapeself.color=colorself.fillcolor=fillcolorself.highlight=Falsedefsethighlight(self,which):self.highlight=bool(which)classEdge:label=Nonedef__init__(self,nodes,tail,head,cnt,*rest):self.tail=nodes[tail]self.head=nodes[head]cnt=int(cnt)self.points=[(float(rest[i]),float(rest[i+1]))foriinrange(0,cnt*2,2)]rest=rest[cnt*2:]iflen(rest)>2:self.label,xl,yl=rest[:3]self.xl=float(xl)self.yl=float(yl)rest=rest[3:]self.style,self.color=restself.highlight=Falseself.cachedbezierpoints=Noneself.cachedarrowhead=Noneself.cachedlimits=Nonedefsethighlight(self,which):self.highlight=bool(which)deflimits(self):result=self.cachedlimitsifresultisNone:points=self.bezierpoints()xs=[point[0]forpointinpoints]ys=[point[1]forpointinpoints]self.cachedlimits=result=(min(xs),max(ys),max(xs),min(ys))returnresultdefbezierpoints(self):result=self.cachedbezierpointsifresultisNone:result=[]pts=self.pointsforiinrange(0,len(pts)-3,3):result+=beziercurve(pts[i],pts[i+1],pts[i+2],pts[i+3])self.cachedbezierpoints=resultreturnresultdefarrowhead(self):result=self.cachedarrowheadifresultisNone:# we don't know if the list of points is in the right order# or not :-( try to guess...defdist(node,pt):returnabs(node.x-pt[0])+abs(node.y-pt[1])error_if_direct=(dist(self.head,self.points[-1])+dist(self.tail,self.points[0]))error_if_reversed=(dist(self.tail,self.points[-1])+dist(self.head,self.points[0]))iferror_if_direct>error_if_reversed:# reversed edgehead=0dir=1else:head=-1dir=-1n=1whileTrue:try:x0,y0=self.points[head]x1,y1=self.points[head+n*dir]exceptIndexError:result=[]breakvx=x0-x1vy=y0-y1try:f=0.12/math.sqrt(vx*vx+vy*vy)vx*=fvy*=fresult=[(x0+0.9*vx,y0+0.9*vy),(x0+0.4*vy,y0-0.4*vx),(x0-0.4*vy,y0+0.4*vx)]breakexcept(ZeroDivisionError,ValueError):n+=1self.cachedarrowhead=resultreturnresultdefbeziercurve((x0,y0),(x1,y1),(x2,y2),(x3,y3),resolution=8):result=[]f=1.0/(resolution-1)append=result.appendforiinrange(resolution):t=f*it0=(1-t)*(1-t)*(1-t)t1=t*(1-t)*(1-t)*3.0t2=t*t*(1-t)*3.0t3=t*t*tappend((x0*t0+x1*t1+x2*t2+x3*t3,y0*t0+y1*t1+y2*t2+y3*t3))returnresultdefsegmentdistance((x0,y0),(x1,y1),(x,y)):"Distance between the point (x,y) and the segment (x0,y0)-(x1,y1)."vx=x1-x0vy=y1-y0try:l=math.hypot(vx,vy)vx/=lvy/=ldlong=vx*(x-x0)+vy*(y-y0)except(ZeroDivisionError,ValueError):dlong=-1ifdlong<0.0:returnmath.hypot(x-x0,y-y0)elifdlong>l:returnmath.hypot(x-x1,y-y1)else:returnabs(vy*(x-x0)-vx*(y-y0))classGraphRenderer:MARGIN=0.6SCALEMIN=3SCALEMAX=100FONTCACHE={}def__init__(self,screen,graphlayout,scale=75):self.graphlayout=graphlayoutself.setscale(scale)self.setoffset(0,0)self.screen=screenself.textzones=[]self.highlightwords=graphlayout.linksself.highlight_word=Noneself.visiblenodes=[]self.visibleedges=[]defwordcolor(self,word):info=self.highlightwords[word]ifisinstance(info,tuple)andlen(info)>=2:color=info[1]else:color=NoneifcolorisNone:color=(128,0,0)ifword==self.highlight_word:return((255,255,80),color)else:return(color,None)defsetscale(self,scale):scale=max(min(scale,self.SCALEMAX),self.SCALEMIN)self.scale=float(scale)w,h=self.graphlayout.boundingboxself.margin=int(self.MARGIN*scale)self.width=int(w*scale)+(2*self.margin)self.height=int(h*scale)+(2*self.margin)self.bboxh=hsize=int(15*(scale-10)/75)self.font=self.getfont(size)defgetfont(self,size):ifsizeinself.FONTCACHE:returnself.FONTCACHE[size]elifsize<5:self.FONTCACHE[size]=NonereturnNoneelse:ifself.graphlayout.fixedfont:filename=FIXEDFONTelse:filename=FONTfont=self.FONTCACHE[size]=pygame.font.Font(filename,size)returnfontdefsetoffset(self,offsetx,offsety):"Set the (x,y) origin of the rectangle where the graph will be rendered."self.ofsx=offsetx-self.marginself.ofsy=offsety-self.margindefshiftoffset(self,dx,dy):self.ofsx+=dxself.ofsy+=dydefgetcenter(self):w,h=self.screen.get_size()returnself.revmap(w//2,h//2)defsetcenter(self,x,y):w,h=self.screen.get_size()x,y=self.map(x,y)self.shiftoffset(x-w//2,y-h//2)defshiftscale(self,factor,fix=None):iffixisNone:fixx,fixy=self.screen.get_size()fixx//=2fixy//=2else:fixx,fixy=fixx,y=self.revmap(fixx,fixy)self.setscale(self.scale*factor)newx,newy=self.map(x,y)self.shiftoffset(newx-fixx,newy-fixy)defreoffset(self,swidth,sheight):offsetx=noffsetx=self.ofsxoffsety=noffsety=self.ofsywidth=self.widthheight=self.height# if it fits, center it, otherwise clampifwidth<=swidth:noffsetx=(width-swidth)//2else:noffsetx=min(max(0,offsetx),width-swidth)ifheight<=sheight:noffsety=(height-sheight)//2else:noffsety=min(max(0,offsety),height-sheight)self.ofsx=noffsetxself.ofsy=noffsetydefgetboundingbox(self):"Get the rectangle where the graph will be rendered."return(-self.ofsx,-self.ofsy,self.width,self.height)defvisible(self,x1,y1,x2,y2):"""Is any part of the box visible (i.e. within the bounding box)? We have to perform clipping ourselves because with big graphs the coordinates may sometimes become longs and cause OverflowErrors within pygame. """w,h=self.screen.get_size()returnx1<wandx2>0andy1<handy2>0defcomputevisible(self):delself.visiblenodes[:]delself.visibleedges[:]w,h=self.screen.get_size()fornodeinself.graphlayout.nodes.values():x,y=self.map(node.x,node.y)nw2=int(node.w*self.scale)//2nh2=int(node.h*self.scale)//2ifx-nw2<wandx+nw2>0andy-nh2<handy+nh2>0:self.visiblenodes.append(node)foredgeinself.graphlayout.edges:x1,y1,x2,y2=edge.limits()x1,y1=self.map(x1,y1)ifx1<wandy1<h:x2,y2=self.map(x2,y2)ifx2>0andy2>0:self.visibleedges.append(edge)defmap(self,x,y):return(int(x*self.scale)-(self.ofsx-self.margin),int((self.bboxh-y)*self.scale)-(self.ofsy-self.margin))defrevmap(self,px,py):return((px+(self.ofsx-self.margin))/self.scale,self.bboxh-(py+(self.ofsy-self.margin))/self.scale)defdraw_node_commands(self,node):xcenter,ycenter=self.map(node.x,node.y)boxwidth=int(node.w*self.scale)boxheight=int(node.h*self.scale)fgcolor=getcolor(node.color,(0,0,0))bgcolor=getcolor(node.fillcolor,(255,255,255))ifnode.highlight:fgcolor=highlight_color(fgcolor)bgcolor=highlight_color(bgcolor)text=node.labellines=text.replace('\\l','\\l\n').replace('\r','\r\n').split('\n')# ignore a final newlineifnotlines[-1]:dellines[-1]wmax=0hmax=0commands=[]bkgndcommands=[]ifself.fontisNone:iflines:raw_line=lines[0].replace('\\l','').replace('\r','')ifraw_line:forsizein(12,10,8,6,4):font=self.getfont(size)img=TextSnippet(self,raw_line,(0,0,0),bgcolor,font=font)w,h=img.get_size()if(w>=boxwidthorh>=boxheight):continueelse:ifw>wmax:wmax=wdefcmd(img=img,y=hmax,w=w):img.draw(xcenter-w//2,ytop+y)commands.append(cmd)hmax+=hbreakelse:forlineinlines:raw_line=line.replace('\\l','').replace('\r','')or' 'if'\f'inraw_line:# grayed out parts of the lineimgs=[]graytext=Trueh=16w_total=0forlinepartinraw_line.split('\f'):graytext=notgraytextifnotlinepart.strip():continueifgraytext:fgcolor=(128,160,160)else:fgcolor=(0,0,0)img=TextSnippet(self,linepart,fgcolor,bgcolor)imgs.append((w_total,img))w,h=img.get_size()w_total+=wifw_total>wmax:wmax=w_totaldefcmd(imgs=imgs,y=hmax):forx,imginimgs:img.draw(xleft+x,ytop+y)commands.append(cmd)else:img=TextSnippet(self,raw_line,(0,0,0),bgcolor)w,h=img.get_size()ifw>wmax:wmax=wifraw_line.strip():ifline.endswith('\\l'):defcmd(img=img,y=hmax):img.draw(xleft,ytop+y)elifline.endswith('\r'):defcmd(img=img,y=hmax,w=w):img.draw(xright-w,ytop+y)else:defcmd(img=img,y=hmax,w=w):img.draw(xcenter-w//2,ytop+y)commands.append(cmd)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 abovexleft=xcenter-wmax//2xright=xcenter+wmax//2ytop=ycenter-hmax//2x=xcenter-boxwidth//2y=ycenter-boxheight//2ifnode.shape=='box':rect=(x-1,y-1,boxwidth+2,boxheight+2)defcmd():self.screen.fill(bgcolor,rect)bkgndcommands.append(cmd)defcmd():pygame.draw.rect(self.screen,fgcolor,rect,1)commands.append(cmd)elifnode.shape=='ellipse':rect=(x-1,y-1,boxwidth+2,boxheight+2)defcmd():pygame.draw.ellipse(self.screen,bgcolor,rect,0)bkgndcommands.append(cmd)defcmd():pygame.draw.ellipse(self.screen,fgcolor,rect,1)commands.append(cmd)elifnode.shape=='octagon':step=1-math.sqrt(2)/2points=[(int(x+boxwidth*fx),int(y+boxheight*fy))forfx,fyin[(step,0),(1-step,0),(1,step),(1,1-step),(1-step,1),(step,1),(0,1-step),(0,step)]]defcmd():pygame.draw.polygon(self.screen,bgcolor,points,0)bkgndcommands.append(cmd)defcmd():pygame.draw.polygon(self.screen,fgcolor,points,1)commands.append(cmd)returnbkgndcommands,commandsdefdraw_commands(self):nodebkgndcmd=[]nodecmd=[]fornodeinself.visiblenodes:cmd1,cmd2=self.draw_node_commands(node)nodebkgndcmd+=cmd1nodecmd+=cmd2edgebodycmd=[]edgeheadcmd=[]foredgeinself.visibleedges:fgcolor=getcolor(edge.color,(0,0,0))ifedge.highlight:fgcolor=highlight_color(fgcolor)points=[self.map(*xy)forxyinedge.bezierpoints()]defdrawedgebody(points=points,fgcolor=fgcolor):pygame.draw.lines(self.screen,fgcolor,False,points)edgebodycmd.append(drawedgebody)points=[self.map(*xy)forxyinedge.arrowhead()]ifpoints:defdrawedgehead(points=points,fgcolor=fgcolor):pygame.draw.polygon(self.screen,fgcolor,points,0)edgeheadcmd.append(drawedgehead)ifedge.label:x,y=self.map(edge.xl,edge.yl)img=TextSnippet(self,edge.label,(0,0,0))w,h=img.get_size()ifself.visible(x-w//2,y-h//2,x+w//2,y+h//2):defdrawedgelabel(img=img,x1=x-w//2,y1=y-h//2):img.draw(x1,y1)edgeheadcmd.append(drawedgelabel)returnedgebodycmd+nodebkgndcmd+edgeheadcmd+nodecmddefrender(self):self.computevisible()bbox=self.getboundingbox()ox,oy,width,height=bboxdpy_width,dpy_height=self.screen.get_size()# some versions of the SDL misinterpret widely out-of-range values,# so clamp themifox<0:width+=oxox=0ifoy<0:height+=oyoy=0ifwidth>dpy_width:width=dpy_widthifheight>dpy_height:height=dpy_heightself.screen.fill((224,255,224),(ox,oy,width,height))# gray off-bkgnd areasgray=(128,128,128)ifox>0:self.screen.fill(gray,(0,0,ox,dpy_height))ifoy>0:self.screen.fill(gray,(0,0,dpy_width,oy))w=dpy_width-(ox+width)ifw>0:self.screen.fill(gray,(dpy_width-w,0,w,dpy_height))h=dpy_height-(oy+height)ifh>0:self.screen.fill(gray,(0,dpy_height-h,dpy_width,h))# draw the graph and record the position of textsdelself.textzones[:]forcmdinself.draw_commands():cmd()deffindall(self,searchstr):"""Return an iterator for all nodes and edges that contain a searchstr. """foriteminself.graphlayout.nodes.itervalues():ifitem.labelandsearchstrinitem.label:yielditemforiteminself.graphlayout.edges:ifitem.labelandsearchstrinitem.label:yielditemdefat_position(self,(x,y)):"""Figure out the word under the cursor."""forrx,ry,rw,rh,wordinself.textzones:ifrx<=x<rx+rwandry<=y<ry+rh:returnwordreturnNonedefnode_at_position(self,(x,y)):"""Return the Node under the cursor."""x,y=self.revmap(x,y)fornodeinself.visiblenodes:if2.0*abs(x-node.x)<=node.wand2.0*abs(y-node.y)<=node.h:returnnodereturnNonedefedge_at_position(self,(x,y),distmax=14):"""Return the Edge near the cursor."""# XXX this function is very CPU-intensive and makes the display kinda sluggishdistmax/=self.scalexy=self.revmap(x,y)closest_edge=Noneforedgeinself.visibleedges:pts=edge.bezierpoints()foriinrange(1,len(pts)):d=segmentdistance(pts[i-1],pts[i],xy)ifd<distmax:distmax=dclosest_edge=edgereturnclosest_edgeclassTextSnippet:def__init__(self,renderer,text,fgcolor,bgcolor=None,font=None):self.renderer=rendererself.imgs=[]self.parts=[]iffontisNone:font=renderer.fontiffontisNone:returnparts=self.partsforwordinre_nonword.split(text):ifnotword:continueifwordinrenderer.highlightwords:fg,bg=renderer.wordcolor(word)bg=bgorbgcolorelse:fg,bg=fgcolor,bgcolorparts.append((word,fg,bg))# consolidate sequences of words with the same colorforiinrange(len(parts)-2,-1,-1):ifparts[i][1:]==parts[i+1][1:]:word,fg,bg=parts[i]parts[i]=word+parts[i+1][0],fg,bgdelparts[i+1]# delete None backgroundsforiinrange(len(parts)):ifparts[i][2]isNone:parts[i]=parts[i][:2]# render partsi=0whilei<len(parts):part=parts[i]word=part[0]try:try:img=font.render(word,False,*part[1:])exceptpygame.error,e:# Try *with* anti-aliasing to work around a bug in SDLimg=font.render(word,True,*part[1:])exceptpygame.error:delparts[i]# Text has zero widthelse:self.imgs.append(img)i+=1defget_size(self):ifself.imgs:sizes=[img.get_size()forimginself.imgs]returnsum([wforw,hinsizes]),max([hforw,hinsizes])else:return0,0defdraw(self,x,y):forpart,imginzip(self.parts,self.imgs):word=part[0]self.renderer.screen.blit(img,(x,y))w,h=img.get_size()self.renderer.textzones.append((x,y,w,h,word))x+=w